diff --git a/src/components/feature/EmptyItem/index.tsx b/src/components/feature/EmptyItem/index.tsx index d0f77791..f74ab9f8 100644 --- a/src/components/feature/EmptyItem/index.tsx +++ b/src/components/feature/EmptyItem/index.tsx @@ -23,6 +23,14 @@ const EMPTY_ITEM_TEXT = { title: `사용한 선물이 없어요`, description: `받은 선물🎁을 사용하고, 친구에게 고마운 마음을 표현해보세요~!`, }, + funding_usable: { + title: `사용 가능한 펀딩이 없어요`, + description: `펀딩받고 싶은 선물🎁이 있나요?\n내 취향에 맞는 선물을 등록해보세요~!`, + }, + funding_used: { + title: `사용한 펀딩이 없어요`, + description: `받은 펀딩을 사용하고, 친구들에게 고마운 마음을 표현해보세요~!`, + }, history_order: { title: `주문내역이 없어요`, description: `소중한 친구에게 마음❤️을 전해보아요~!`, diff --git a/src/components/ui/Tabs/index.module.scss b/src/components/ui/Tabs/index.module.scss index 590b4cd6..69519e9b 100644 --- a/src/components/ui/Tabs/index.module.scss +++ b/src/components/ui/Tabs/index.module.scss @@ -49,7 +49,8 @@ } } -.product_list { +.product_list, +.received_box { .wrapper_tab { margin-right: 33px; padding: 14px 0; @@ -67,6 +68,10 @@ } } +.received_box { + margin-bottom: 30px; +} + .funding_history { .wrapper_tab { width: 50%; diff --git a/src/components/ui/Tabs/index.tsx b/src/components/ui/Tabs/index.tsx index 4ef50dfa..1d5a70c4 100644 --- a/src/components/ui/Tabs/index.tsx +++ b/src/components/ui/Tabs/index.tsx @@ -8,7 +8,7 @@ import styles from './index.module.scss'; type TabProps = { initialTabId: Tab['id']; tabs: Tab[]; - mode: 'product_list' | 'product_detail' | 'funding_history'; + mode: 'product_list' | 'product_detail' | 'funding_history' | 'received_box'; }; const Tabs = ({ initialTabId = 0, tabs, mode }: TabProps) => { diff --git a/src/layouts/MyPage/FundingBox/FundingTab/index.module.scss b/src/layouts/MyPage/FundingBox/FundingTab/index.module.scss new file mode 100644 index 00000000..90bee1be --- /dev/null +++ b/src/layouts/MyPage/FundingBox/FundingTab/index.module.scss @@ -0,0 +1,7 @@ +.list_funding { + display: grid; + grid-template-columns: repeat(4, 1fr); + row-gap: 30px; + max-width: 840px; + margin: 30px 0 100px; +} diff --git a/src/layouts/MyPage/FundingBox/FundingTab/index.tsx b/src/layouts/MyPage/FundingBox/FundingTab/index.tsx new file mode 100644 index 00000000..4169fe9e --- /dev/null +++ b/src/layouts/MyPage/FundingBox/FundingTab/index.tsx @@ -0,0 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useEffect, useState } from 'react'; + +import EmptyItem from 'components/feature/EmptyItem'; +import Spinner from 'components/ui/Spinner'; + +import { useInfinityScroll } from 'hooks/useInfinityScroll'; +import { getMyFundingItems } from 'services/api/v1/funding'; + +import { MyFundingItemType } from 'types/funding'; + +import MyFundingItem from '../MyFundingItem'; + +import styles from './index.module.scss'; + +type FundingTabProps = { + status: 'USABLE' | 'USED'; +}; + +const FundingTab = ({ status }: FundingTabProps) => { + const [fundingItems, setFundingItems] = useState([]); + const [hasNext, setHasNext] = useState(true); + const [page, setPage] = useState(0); + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['fundingItem', status], + queryFn: () => getMyFundingItems(status), + }); + + const observingTarget = useInfinityScroll(() => { + if (data) setPage(data.pageNumber + 1); + }, hasNext); + + useEffect(() => { + refetch(); + }, [page]); + + useEffect(() => { + if (data) { + setFundingItems((prev) => [...prev, ...data.items]); + setHasNext(data.hasNext); + } + }, [data]); + + if (!hasNext && fundingItems.length === 0) { + const emptyType = status === 'USABLE' ? 'funding_usable' : 'funding_used'; + + return ; + } + + return ( + <> +
    + {fundingItems.map((fundingItem) => ( +
  • + +
  • + ))} +
+ {isLoading && } + {hasNext &&
} + + ); +}; + +export default FundingTab; diff --git a/src/layouts/MyPage/FundingBox/MyFundingItem/index.module.scss b/src/layouts/MyPage/FundingBox/MyFundingItem/index.module.scss new file mode 100644 index 00000000..d22023d1 --- /dev/null +++ b/src/layouts/MyPage/FundingBox/MyFundingItem/index.module.scss @@ -0,0 +1,67 @@ +@import 'styles/mixins.module'; + +.badge { + position: absolute; + top: 10px; + right: 10px; + + &.used { + @include img-badge(0, -55px, 50px, 50px); + } + + &.canceled { + @include img-badge(-55px, -55px, 50px, 50px); + } +} + +.d_day { + display: inline-block; + position: absolute; + top: 0; + left: 0; + background-color: #888; + color: #fff; + font-size: 14px; + padding: 5px 8px; + letter-spacing: -0.03em; +} + +.wrapper_funding { + $width: 196px; + + width: $width; + position: relative; + + .wrapper_thumb { + @include wrapper-prod-img($width, 0); + + .img_unavailable { + opacity: 0.4; + } + } + + .txt_brand { + display: block; + padding-top: 9px; + font-size: 13px; + color: #5990c7; + + @include text-ellipsis(1); + } + + .txt_prod { + display: block; + padding-top: 4px; + font-size: 15px; + color: #666; + + @include text-ellipsis(1); + } +} + +.txt_date { + margin-top: 8px; + font-size: 12px; + color: #a0a0a0; + letter-spacing: -0.028em; +} diff --git a/src/layouts/MyPage/FundingBox/MyFundingItem/index.tsx b/src/layouts/MyPage/FundingBox/MyFundingItem/index.tsx new file mode 100644 index 00000000..6b179929 --- /dev/null +++ b/src/layouts/MyPage/FundingBox/MyFundingItem/index.tsx @@ -0,0 +1,50 @@ +import clsx from 'clsx'; + +import { getDDay, getOneYearLaterDate } from 'utils/generate'; + +import { + MyFundingItemType, + STATUS_TEXT, + FundingItemStatusType, +} from 'types/funding'; + +import styles from './index.module.scss'; + +type MyFundingItemProps = { + fundingItem: MyFundingItemType; + status: 'USABLE' | 'USED'; +}; + +const MyFundingItem = ({ fundingItem, status }: MyFundingItemProps) => { + const { product, receivedDate } = fundingItem; + const { name, photo, brandName } = product; + const expiredAt = getOneYearLaterDate(receivedDate).toString(); + + return ( +
+
+ {name} +
+ {brandName} + {name} + + {status === 'USABLE' ? ( + D-{getDDay(expiredAt)} + ) : ( + + {STATUS_TEXT[status as FundingItemStatusType]} + + )} + + + {new Date(receivedDate).toLocaleString()} + +
+ ); +}; + +export default MyFundingItem; diff --git a/src/layouts/MyPage/GiftBox/GiftItem/index.module.scss b/src/layouts/MyPage/GiftBox/GiftItem/index.module.scss index 7703e26b..6c4f9690 100644 --- a/src/layouts/MyPage/GiftBox/GiftItem/index.module.scss +++ b/src/layouts/MyPage/GiftBox/GiftItem/index.module.scss @@ -1,12 +1,5 @@ @import 'styles/mixins.module'; -@mixin img-badge($x, $y, $width, $height) { - @include img-sprite($x, $y, $width, $height); - - background-image: url('assets/badge_state.png'); - background-size: 325px 105px; -} - .badge { position: absolute; top: 10px; diff --git a/src/layouts/MyPage/GiftBox/GiftItem/index.tsx b/src/layouts/MyPage/GiftBox/GiftItem/index.tsx index 4308445b..dc010c3b 100644 --- a/src/layouts/MyPage/GiftBox/GiftItem/index.tsx +++ b/src/layouts/MyPage/GiftBox/GiftItem/index.tsx @@ -1,5 +1,7 @@ import clsx from 'clsx'; +import { getDDay } from 'utils/generate'; + import { Gift, STATUS_TEXT, StatusType } from 'types/Gift'; import styles from './index.module.scss'; @@ -16,12 +18,6 @@ const GiftItem = ({ gift, status }: GiftItemProps) => { receivedAt, } = gift; - const getDDay = () => { - const diffTime = new Date(expiredAt).getTime() - new Date().getTime(); // 시간 차이, 단위: ms - const DDay = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); // 밀리초 / 1일 - return DDay; - }; - return (
@@ -35,7 +31,7 @@ const GiftItem = ({ gift, status }: GiftItemProps) => { {productName} {status === 'NOT_USED' ? ( - D-{getDDay()} + D-{getDDay(expiredAt)} ) : ( {STATUS_TEXT[status]} diff --git a/src/pages/MyPage/FundingBox/index.tsx b/src/pages/MyPage/FundingBox/index.tsx index d990e6ec..cce2ea40 100644 --- a/src/pages/MyPage/FundingBox/index.tsx +++ b/src/pages/MyPage/FundingBox/index.tsx @@ -1,15 +1,48 @@ +import { useQuery } from '@tanstack/react-query'; + +import Spinner from 'components/ui/Spinner'; +import Tabs from 'components/ui/Tabs'; +import FundingTab from 'layouts/MyPage/FundingBox/FundingTab'; + +import { getMyFundingItems } from 'services/api/v1/funding'; + +import { Tab } from 'types/tab'; + import styles from './index.module.scss'; const FundingBox = () => { + const { data, isLoading } = useQuery({ + queryKey: ['fundingItem'], + queryFn: () => getMyFundingItems(), + }); + + const tabs: Tab[] = [ + { + id: 0, + name: `사용 가능한 펀딩아이템`, + content: , + }, + { + id: 1, + name: `사용 완료한 펀딩아이템 `, + content: , + }, + ]; + return ( - <> -

- 총 n개의 -
- 펀딩을 달성했습니다. 🎉 -

- 탭 - +
+ {isLoading && } + {data && ( + <> +

+ 총 {`${data.totalElements}`}개의 +
+ 펀딩을 달성했습니다. 🎉 +

+ + + )} +
); }; diff --git a/src/pages/MyPage/GiftBox/index.tsx b/src/pages/MyPage/GiftBox/index.tsx index a7d79d46..5c6d3133 100644 --- a/src/pages/MyPage/GiftBox/index.tsx +++ b/src/pages/MyPage/GiftBox/index.tsx @@ -26,7 +26,7 @@ const GiftBox = () => {
확인해보세요 🎁 - + ); }; diff --git a/src/services/api/v1/funding.ts b/src/services/api/v1/funding.ts new file mode 100644 index 00000000..53c658a4 --- /dev/null +++ b/src/services/api/v1/funding.ts @@ -0,0 +1,18 @@ +import { PaginationResponse } from 'types/PaginationResponse'; +import { MyFundingItemType } from 'types/funding'; + +import { apiV1 } from '.'; + +export const getMyFundingItems = async (status?: string) => { + const baseUrl = `/funding/gift`; + + if (status) { + const myFundingItems = await apiV1.get(`${baseUrl}?status=${status}`); + + return myFundingItems.data as PaginationResponse; + } + + const myFundingItems = await apiV1.get(baseUrl); + + return myFundingItems.data as PaginationResponse; +}; diff --git a/src/stories/MyFundingItem.stories.tsx b/src/stories/MyFundingItem.stories.tsx new file mode 100644 index 00000000..fbd0a416 --- /dev/null +++ b/src/stories/MyFundingItem.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { MemoryRouter } from 'react-router-dom'; + +import MyFundingItem from 'layouts/MyPage/FundingBox/MyFundingItem'; + +import { MyFundingItemType } from 'types/funding'; + +const meta: Meta = { + title: 'MyFundingItem', + component: MyFundingItem, + decorators: [ + (Story) => ( + + + + ), + ], + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +const fundingItem: MyFundingItemType = { + id: 1, + product: { + productId: 386515, + name: '디핀다트 구슬아이스크림 기프트팩(50ml* 12개)', + photo: + 'https://st.kakaocdn.net/product/gift/product/20220422165729_0119e39ca8a14084a3504b85ca4eaf30.jpeg', + price: 15900, + brandName: '디핀다트', + }, + quantity: 1, + receivedDate: '2024-04-24T14:59:42.48152', +}; + +export const NotUsedFundingItem: Story = { + args: { fundingItem }, +}; diff --git a/src/styles/_mixins.module.scss b/src/styles/_mixins.module.scss index 0a8d3f47..ac6064d8 100644 --- a/src/styles/_mixins.module.scss +++ b/src/styles/_mixins.module.scss @@ -186,3 +186,10 @@ background-image: url('assets/ico_giftorder.png'); background-size: 165px 210px; } + +@mixin img-badge($x, $y, $width, $height) { + @include img-sprite($x, $y, $width, $height); + + background-image: url('assets/badge_state.png'); + background-size: 325px 105px; +} diff --git a/src/types/funding.ts b/src/types/funding.ts new file mode 100644 index 00000000..1e6b14d8 --- /dev/null +++ b/src/types/funding.ts @@ -0,0 +1,19 @@ +export const STATUS_TEXT = { + USABLE: '미사용', + USED: '사용완료', +} as const; + +export type FundingItemStatusType = keyof typeof STATUS_TEXT; + +export type MyFundingItemType = { + id: number; + product: { + productId: number; + name: string; + photo: string; + price: number; + brandName: string; + }; + quantity: number; + receivedDate: string; +}; diff --git a/src/utils/generate.ts b/src/utils/generate.ts index 6bd3cb87..bd808df0 100644 --- a/src/utils/generate.ts +++ b/src/utils/generate.ts @@ -2,8 +2,8 @@ export function getRandomNumber(start: number, end: number): number { return Math.floor(start + Math.random() * end); } -export const getOneYearLaterDate = () => { - const currentDate = new Date(); +export const getOneYearLaterDate = (date?: string) => { + const currentDate = date ? new Date(date) : new Date(); const oneYearLaterDate = new Date( currentDate.getFullYear() + 1, currentDate.getMonth(), @@ -21,3 +21,10 @@ export const getHalfYearEarlierDate = () => { ); return halfYearEarlierDate; }; + +export const getDDay = (date: string): number => { + const diffTime = new Date(date).getTime() - new Date().getTime(); // 시간 차이, 단위: ms + const DDay = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); // 밀리초 / 1일 + + return DDay; +};