diff --git a/.storybook/main.ts b/.storybook/main.ts index 9dbdf6fc9..15fc10fed 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,6 +1,7 @@ import type { StorybookConfig } from '@storybook/nextjs'; const config: StorybookConfig = { stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], + staticDirs: ['../public'], addons: [ '@storybook/addon-links', '@storybook/addon-essentials', diff --git a/components/modal/LoadingButton.tsx b/components/modal/LoadingButton.tsx new file mode 100644 index 000000000..8d2c0b262 --- /dev/null +++ b/components/modal/LoadingButton.tsx @@ -0,0 +1,13 @@ +import styles from 'styles/modal/LoadingButton.module.scss'; + +export default function LoadingButton() { + return ( +
+
+ o + o + o +
+
+ ); +} diff --git a/components/modal/ModalButton.tsx b/components/modal/ModalButton.tsx index 33aba201f..c248c8e0f 100644 --- a/components/modal/ModalButton.tsx +++ b/components/modal/ModalButton.tsx @@ -1,9 +1,11 @@ +import LoadingButton from 'components/modal/LoadingButton'; import styles from 'styles/modal/Modal.module.scss'; type ButtonProps = { style: 'positive' | 'negative'; value: string; form?: string; + isLoading?: boolean; onClick: () => void; }; @@ -15,10 +17,20 @@ export function ModalButtonContainer({ return
{children}
; } -export function ModalButton({ style, value, onClick, form }: ButtonProps) { +export function ModalButton({ + style, + value, + onClick, + form, + isLoading, +}: ButtonProps) { return (
- + {isLoading ? ( + + ) : ( + + )}
); } diff --git a/components/modal/store/CoinHistoryContainer.tsx b/components/modal/store/CoinHistoryContainer.tsx index d2ac57a83..e443e317c 100644 --- a/components/modal/store/CoinHistoryContainer.tsx +++ b/components/modal/store/CoinHistoryContainer.tsx @@ -1,5 +1,6 @@ import { ICoinHistory } from 'types/userTypes'; import CoinHistoryDetails from 'components/modal/store/CoinHistoryDetails'; +import ErrorEmoji from 'public/image/noti_empty.svg'; import styles from 'styles/modal/store/CoinHistoryContainer.module.scss'; type CoinHistoryProps = { @@ -12,14 +13,20 @@ export default function CoinHistoryContainer({ return (
{useCoinList.length === 0 ? ( -
GG코인 내역이 존재하지 않습니다.
+
+
GG코인 내역이 존재하지 않습니다.
+ +
) : ( - useCoinList.map((coinHistory) => ( - - )) + useCoinList.map( + (coinHistory) => + coinHistory.amount !== 0 && ( + + ) + ) )}
); diff --git a/components/modal/store/UserCoinHistoryModal.tsx b/components/modal/store/UserCoinHistoryModal.tsx index 535c904e1..5c4705f35 100644 --- a/components/modal/store/UserCoinHistoryModal.tsx +++ b/components/modal/store/UserCoinHistoryModal.tsx @@ -15,6 +15,7 @@ import styles from 'styles/modal/store/UserCoinHistoryModal.module.scss'; export default function UserCoinHistoryModal({ coin }: ICoin) { const setModal = useSetRecoilState(modalState); + const [isLoading, setIsLoading] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [coinHistoryList, setCoinHistoryList] = useState({ useCoinList: [], @@ -33,8 +34,8 @@ export default function UserCoinHistoryModal({ coin }: ICoin) { getCoinHistoryList(); }, [currentPage]); - // 현재는 출석만 되는 상태 const getCoinHistoryList = async () => { + setIsLoading(true); try { const res = await instance.get( `pingpong/users/coinhistory/?page=${currentPage}&size=5` @@ -46,7 +47,9 @@ export default function UserCoinHistoryModal({ coin }: ICoin) { }); } catch (e) { setError('HB06'); + closeModal(); } + setIsLoading(false); }; return ( @@ -56,7 +59,15 @@ export default function UserCoinHistoryModal({ coin }: ICoin) {
현재 코인
- + {isLoading ? ( +
+ * + * + * +
+ ) : ( + + )}
(false); const resetModal = useResetRecoilState(modalState); const setModal = useSetRecoilState(modalState); const setError = useSetRecoilState(errorState); const gachaAction = async () => { + setIsLoading(true); const data: UseItemRequest = { receiptId: receiptId, }; @@ -39,9 +42,11 @@ export default function ChangeProfileBackgroundModal({ color: res.data.background, }, }); + setIsLoading(false); } catch (error) { // TODO: 에러 코드 확인 후 수정 alert('뽑기에 실패했습니다(˃̣̣̥ᴖ˂̣̣̥) 관리자에게 문의해주세요'); + setIsLoading(false); setError('HB05'); resetModal(); } @@ -57,15 +62,12 @@ export default function ChangeProfileBackgroundModal({
- resetModal()} - /> + gachaAction()} + isLoading={isLoading} + onClick={gachaAction} /> diff --git a/components/modal/store/inventory/ChangeProfileEdgeModal.tsx b/components/modal/store/inventory/ChangeProfileEdgeModal.tsx index 397e96a30..305d85180 100644 --- a/components/modal/store/inventory/ChangeProfileEdgeModal.tsx +++ b/components/modal/store/inventory/ChangeProfileEdgeModal.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useResetRecoilState, useSetRecoilState } from 'recoil'; import { UseItemRequest } from 'types/inventoryTypes'; import { Modal } from 'types/modalTypes'; @@ -22,11 +23,13 @@ const caution = [ export default function ChangeProfileEdgeModal({ receiptId, }: ChangeProfileEdgeModalProps) { + const [isLoading, setIsLoading] = useState(false); const resetModal = useResetRecoilState(modalState); const setModal = useSetRecoilState(modalState); const setError = useSetRecoilState(errorState); const gachaAction = async () => { + setIsLoading(true); const data: UseItemRequest = { receiptId: receiptId, }; @@ -39,12 +42,15 @@ export default function ChangeProfileEdgeModal({ color: res.data.edge, }, }); + setIsLoading(false); } catch (error) { // TODO: 에러 코드 확인 후 수정 alert('뽑기에 실패했습니다(˃̣̣̥ᴖ˂̣̣̥) 관리자에게 문의해주세요'); + setIsLoading(false); setError('HB04'); resetModal(); } + setIsLoading(false); }; return ( @@ -57,15 +63,12 @@ export default function ChangeProfileEdgeModal({ - resetModal()} - /> + gachaAction()} + isLoading={isLoading} + onClick={gachaAction} /> diff --git a/components/modal/store/purchase/BuyModal.tsx b/components/modal/store/purchase/BuyModal.tsx index ea0bef24d..1b766df0c 100644 --- a/components/modal/store/purchase/BuyModal.tsx +++ b/components/modal/store/purchase/BuyModal.tsx @@ -1,22 +1,36 @@ -import { useEffect, useState } from 'react'; -import { Purchase } from 'types/itemTypes'; +import { useState } from 'react'; +import { useSetRecoilState, useResetRecoilState } from 'recoil'; import { PriceTag } from 'types/modalTypes'; +import { instance } from 'utils/axios'; +import { errorState } from 'utils/recoil/error'; +import { modalState } from 'utils/recoil/modal'; +import { updateCoinState } from 'utils/recoil/updateCoin'; import { ModalButtonContainer, ModalButton, } from 'components/modal/ModalButton'; -import useBuyModal from 'hooks/modal/store/purchase/useBuyModal'; import styles from 'styles/modal/store/BuyModal.module.scss'; export default function BuyModal({ itemId, product, price }: PriceTag) { - const [purchaseItem, setPurchaseItem] = useState({ itemId: -1 }); - const { onPurchase, onCancel } = useBuyModal(purchaseItem); + const [isLoading, setIsLoading] = useState(false); + const resetModal = useResetRecoilState(modalState); + const setError = useSetRecoilState(errorState); + const updateCoin = useSetRecoilState(updateCoinState); - useEffect(() => { - setPurchaseItem({ - itemId: itemId, - }); - }, [itemId]); + // TODO: 에러 처리 + const onPurchase = async () => { + setIsLoading(true); + try { + await instance.post(`/pingpong/items/purchases/${itemId}`, null); + alert(`구매 성공!`); + updateCoin(true); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + setError('HB03'); + } + resetModal(); + }; return (
@@ -38,8 +52,13 @@ export default function BuyModal({ itemId, product, price }: PriceTag) {
- - + + ); diff --git a/components/modal/store/purchase/GiftModal.tsx b/components/modal/store/purchase/GiftModal.tsx index 53a1b31a5..0dd5e67c9 100644 --- a/components/modal/store/purchase/GiftModal.tsx +++ b/components/modal/store/purchase/GiftModal.tsx @@ -1,28 +1,45 @@ -import { useEffect, useState } from 'react'; -import { Gift } from 'types/itemTypes'; +import { useState } from 'react'; +import { useSetRecoilState, useResetRecoilState } from 'recoil'; +import { GiftRequest } from 'types/itemTypes'; import { PriceTag } from 'types/modalTypes'; +import { instance } from 'utils/axios'; +import { errorState } from 'utils/recoil/error'; +import { modalState } from 'utils/recoil/modal'; +import { updateCoinState } from 'utils/recoil/updateCoin'; import { ModalButtonContainer, ModalButton, } from 'components/modal/ModalButton'; import GiftSearchBar from 'components/store/purchase/GiftSearchBar'; -import useGiftModal from 'hooks/modal/store/purchase/useGiftModal'; import styles from 'styles/modal/store/GiftModal.module.scss'; export default function GiftModal({ itemId, product, price }: PriceTag) { - const [recipient, setRecipient] = useState(''); - const [gift, setGift] = useState({ - itemId: -1, + const [isLoading, setIsLoading] = useState(false); + const resetModal = useResetRecoilState(modalState); + const setError = useSetRecoilState(errorState); + const updateCoin = useSetRecoilState(updateCoinState); + const [giftReqData, setGiftReqData] = useState({ ownerId: '', }); - const { onPurchase, onCancel } = useGiftModal(gift); - useEffect(() => { - setGift({ - itemId: itemId, - ownerId: recipient, - }); - }, [itemId, recipient]); + // TODO: 에러 처리 + const onPurchase = async () => { + if (giftReqData.ownerId === '') { + alert('선물할 유저를 선택해주세요.'); + return; + } + setIsLoading(true); + try { + await instance.post(`/pingpong/items/gift/${itemId}`, giftReqData); + alert(`${giftReqData.ownerId}님께 선물이 전달되었습니다.`); + updateCoin(true); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + setError('HB02'); + } + resetModal(); + }; return (
@@ -39,10 +56,10 @@ export default function GiftModal({ itemId, product, price }: PriceTag) {
{price} 코인
- - {recipient !== '' && ( + + {giftReqData.ownerId !== '' && (
- {recipient}님에게 선물하시겠습니까? + {giftReqData.ownerId}님에게 선물하시겠습니까?
)}
@@ -50,8 +67,13 @@ export default function GiftModal({ itemId, product, price }: PriceTag) {
- - + + ); diff --git a/components/store/purchase/GiftSearchBar.tsx b/components/store/purchase/GiftSearchBar.tsx index 26aa1babf..1daf00a80 100644 --- a/components/store/purchase/GiftSearchBar.tsx +++ b/components/store/purchase/GiftSearchBar.tsx @@ -1,13 +1,14 @@ import { useEffect, Dispatch, SetStateAction } from 'react'; import { GoSearch } from 'react-icons/go'; import { IoIosCloseCircle } from 'react-icons/io'; +import { GiftRequest } from 'types/itemTypes'; import useSearchBar from 'hooks/useSearchBar'; import styles from 'styles/main/SearchBar.module.scss'; export default function GiftSearchBar({ - setRecipient, + setGiftReqData, }: { - setRecipient: Dispatch>; + setGiftReqData: Dispatch>; }) { const { keyword, @@ -17,12 +18,13 @@ export default function GiftSearchBar({ setShowDropDown, searchResult, searchBarRef, - handleKeyDown, } = useSearchBar(); useEffect(() => { if (keyword === '') { - setRecipient(''); + setGiftReqData({ + ownerId: '', + }); } }, [keyword]); @@ -31,10 +33,26 @@ export default function GiftSearchBar({ intraId: string ) => { setKeyword(intraId); - setRecipient(intraId); + setGiftReqData({ + ownerId: intraId, + }); setShowDropDown(false); }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + searchResult.map((data) => { + if (data === keyword) { + setShowDropDown(false); + event.currentTarget.blur(); + setGiftReqData({ + ownerId: data, + }); + } + }); + } + }; + return (
{ - const setModal = useSetRecoilState(modalState); - const setError = useSetRecoilState(errorState); - const updateCoin = useSetRecoilState(updateCoinState); - - const onPurchase = async () => { - try { - await instance.post( - `/pingpong/items/purchases/${purchasedItem.itemId}`, - null - ); - alert(`구매 성공!`); - updateCoin(true); - } catch (error) { - setError('HB03'); - } - setModal({ modalName: null }); - }; - - const onCancel = () => { - setModal({ modalName: null }); - }; - - return { onPurchase, onCancel }; -}; - -export default useBuyModal; diff --git a/hooks/modal/store/purchase/useGiftModal.ts b/hooks/modal/store/purchase/useGiftModal.ts deleted file mode 100644 index 781b9039e..000000000 --- a/hooks/modal/store/purchase/useGiftModal.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useSetRecoilState } from 'recoil'; -import { Gift, GiftRequest } from 'types/itemTypes'; -import { instance } from 'utils/axios'; -import { errorState } from 'utils/recoil/error'; -import { modalState } from 'utils/recoil/modal'; -import { updateCoinState } from 'utils/recoil/updateCoin'; - -const useGiftModal = (gift: Gift) => { - const setModal = useSetRecoilState(modalState); - const setError = useSetRecoilState(errorState); - const updateCoin = useSetRecoilState(updateCoinState); - const data: GiftRequest = { - ownerId: gift.ownerId, - }; - - const onPurchase = async () => { - if (gift.ownerId === '') { - alert('선물할 유저를 선택해주세요.'); - return; - } - try { - const res = await instance.post( - `/pingpong/items/gift/${gift.itemId}`, - data - ); - if (res.status === 201) { - alert(`${gift.ownerId}님께 선물이 전달되었습니다.`); - updateCoin(true); - } - } catch (error) { - setError('HB02'); - } - setModal({ modalName: null }); - }; - - const onCancel = () => { - setModal({ modalName: null }); - }; - - return { onPurchase, onCancel }; -}; - -export default useGiftModal; diff --git a/stories/store/BuyModal.stories.tsx b/stories/store/BuyModal.stories.tsx new file mode 100644 index 000000000..9ca980606 --- /dev/null +++ b/stories/store/BuyModal.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import BuyModal from 'components/modal/store/purchase/BuyModal'; + +const meta: Meta = { + title: 'Modal/BuyModal', + component: BuyModal, + tags: ['autodocs'], + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + itemId: 1, + product: '프사 변경', + price: 84, + }, +}; diff --git a/stories/store/CoinHistoryModal.stories.tsx b/stories/store/CoinHistoryModal.stories.tsx new file mode 100644 index 000000000..62de03ab7 --- /dev/null +++ b/stories/store/CoinHistoryModal.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import UserCoinHistoryModal from 'components/modal/store/UserCoinHistoryModal'; + +const meta: Meta = { + title: 'Modal/UserCoinHistoryModal', + component: UserCoinHistoryModal, + tags: ['autodocs'], + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + coin: 100, + }, +}; diff --git a/stories/store/GiftModal.stories.tsx b/stories/store/GiftModal.stories.tsx new file mode 100644 index 000000000..a2622b9ef --- /dev/null +++ b/stories/store/GiftModal.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import GiftModal from 'components/modal/store/purchase/GiftModal'; + +const meta: Meta = { + title: 'Modal/GiftModal', + component: GiftModal, + tags: ['autodocs'], + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + itemId: 1, + product: '프사 변경', + price: 84, + }, +}; diff --git a/styles/common.scss b/styles/common.scss index d8b04153c..2281da97a 100644 --- a/styles/common.scss +++ b/styles/common.scss @@ -339,6 +339,13 @@ $text-shadow-blue: 2px 2px 0px $pp-blue; } } +@mixin waitAnimation($delay) { + @include spanUpDownAnimation(0.2rem, 0.2rem); + position: relative; + display: inline-block; + animation-delay: $delay; +} + @mixin modalButton { display: flex; justify-content: center; diff --git a/styles/modal/LoadingButton.module.scss b/styles/modal/LoadingButton.module.scss new file mode 100644 index 000000000..89b2648ea --- /dev/null +++ b/styles/modal/LoadingButton.module.scss @@ -0,0 +1,25 @@ +@import 'styles/common.scss'; + +.loadingButton { + display: flex; + width: 8rem; + height: 2.5rem; + font-size: 0.9rem; + color: rgba(255, 255, 255, 1); + background: linear-gradient(180deg, #c66bf2 0%, rgba(96, 6, 138, 0.52) 100%); + border: 0; + border-radius: 0.625rem; + justify-content: center; + align-items: center; + .loading { + .span1 { + @include waitAnimation(0.3s); + } + .span2 { + @include waitAnimation(0.5s); + } + .span3 { + @include waitAnimation(0.7s); + } + } +} diff --git a/styles/modal/store/CoinHistoryContainer.module.scss b/styles/modal/store/CoinHistoryContainer.module.scss index 45bfd67be..f941720fa 100644 --- a/styles/modal/store/CoinHistoryContainer.module.scss +++ b/styles/modal/store/CoinHistoryContainer.module.scss @@ -7,11 +7,16 @@ flex-direction: column; padding-top: 1rem; overflow-y: scroll; - border-top: 1px solid #3b363b36; .empty { display: flex; - justify-content: center; + flex-direction: column; font-size: 1.2rem; color: $gray; + div { + margin: 2rem auto; + } + svg { + margin: 1.5rem auto; + } } } diff --git a/styles/modal/store/UserCoinHistoryModal.module.scss b/styles/modal/store/UserCoinHistoryModal.module.scss index 4815d651a..c619a94f2 100644 --- a/styles/modal/store/UserCoinHistoryModal.module.scss +++ b/styles/modal/store/UserCoinHistoryModal.module.scss @@ -16,12 +16,31 @@ .balance { display: flex; width: 80%; + padding-bottom: 0.5rem; margin-bottom: 0.5rem; font-size: 1.3rem; color: #ffffff; + border-bottom: 1px solid #3b363b36; justify-content: space-between; .current { padding-top: 0.35rem; } } + + .loading { + margin: 5rem 0; + color: #ffffff; + .span1 { + @include waitAnimation(0.3s); + font-size: 1.8rem; + } + .span2 { + @include waitAnimation(0.5s); + font-size: 1.8rem; + } + .span3 { + @include waitAnimation(0.7s); + font-size: 1.8rem; + } + } }