From 512995ecb12e729f4b3f86e1a5e09c82f4c2e92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:08:22 +0900 Subject: [PATCH] feat/mypage-apis --- .../(tabs)/mypage/_components/AccountCard.tsx | 47 ++++----- .../mypage/_components/ProfileSection.tsx | 87 ++++++++++++----- .../mypage/_components/RewardQrModal.tsx | 96 +++++++++++++++++++ .../(tabs)/mypage/_components/StatsCard.tsx | 23 ++++- apps/customer/src/app/(tabs)/mypage/page.tsx | 42 +++++++- apps/customer/src/shared/api/api.ts | 2 + .../mutation/auth/useLogoutMutation.ts | 10 ++ .../queries/query/member/useMyPageQuery.ts | 8 ++ .../queries/query/member/useRewardQrQuery.ts | 16 ++++ 9 files changed, 274 insertions(+), 57 deletions(-) create mode 100644 apps/customer/src/app/(tabs)/mypage/_components/RewardQrModal.tsx create mode 100644 apps/customer/src/shared/queries/mutation/auth/useLogoutMutation.ts create mode 100644 apps/customer/src/shared/queries/query/member/useMyPageQuery.ts create mode 100644 apps/customer/src/shared/queries/query/member/useRewardQrQuery.ts diff --git a/apps/customer/src/app/(tabs)/mypage/_components/AccountCard.tsx b/apps/customer/src/app/(tabs)/mypage/_components/AccountCard.tsx index 7a85ccd..1a5612b 100644 --- a/apps/customer/src/app/(tabs)/mypage/_components/AccountCard.tsx +++ b/apps/customer/src/app/(tabs)/mypage/_components/AccountCard.tsx @@ -1,37 +1,25 @@ "use client"; import { useState } from "react"; +import { useRouter } from "next/navigation"; import { Card } from "@compasser/design-system"; import { ConfirmActionModal } from "./ConfirmActionModal"; +import { useLogoutMutation } from "@/shared/queries/mutation/auth/useLogoutMutation"; export const AccountCard = () => { + const router = useRouter(); const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); - const handleOpenLogoutModal = () => { - setIsLogoutModalOpen(true); - }; - - const handleCloseLogoutModal = () => { - setIsLogoutModalOpen(false); - }; - - const handleOpenWithdrawModal = () => { - setIsWithdrawModalOpen(true); - }; - - const handleCloseWithdrawModal = () => { - setIsWithdrawModalOpen(false); - }; + const { mutate: logout, isPending } = useLogoutMutation(); const handleLogout = () => { - console.log("로그아웃"); - setIsLogoutModalOpen(false); - }; - - const handleWithdraw = () => { - console.log("회원탈퇴"); - setIsWithdrawModalOpen(false); + logout(undefined, { + onSuccess: () => { + setIsLogoutModalOpen(false); + router.replace("/login"); + }, + }); }; return ( @@ -44,7 +32,7 @@ export const AccountCard = () => {
+ - - - + + ); }; \ No newline at end of file diff --git a/apps/customer/src/app/(tabs)/mypage/_components/RewardQrModal.tsx b/apps/customer/src/app/(tabs)/mypage/_components/RewardQrModal.tsx new file mode 100644 index 0000000..7312dfa --- /dev/null +++ b/apps/customer/src/app/(tabs)/mypage/_components/RewardQrModal.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Icon } from "@compasser/design-system"; +import { useRewardQrQuery } from "@/shared/queries/query/member/useRewardQrQuery"; + +interface RewardQrModalProps { + open: boolean; + onClose: () => void; +} + +export const RewardQrModal = ({ open, onClose }: RewardQrModalProps) => { + const { data, isLoading, isFetching, dataUpdatedAt } = useRewardQrQuery({ + enabled: open, + }); + + const [secondsLeft, setSecondsLeft] = useState(60); + + const qrImageUrl = useMemo(() => { + if (!data) return null; + return URL.createObjectURL(data); + }, [data]); + + useEffect(() => { + return () => { + if (qrImageUrl) { + URL.revokeObjectURL(qrImageUrl); + } + }; + }, [qrImageUrl]); + + useEffect(() => { + if (!open) return; + + setSecondsLeft(60); + + const interval = window.setInterval(() => { + const elapsed = Math.floor((Date.now() - dataUpdatedAt) / 1000); + const remain = Math.max(60 - elapsed, 0); + setSecondsLeft(remain); + }, 1000); + + return () => { + window.clearInterval(interval); + }; + }, [open, dataUpdatedAt]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > +
+ + +

+ 사장님께 QR을 보여주세요. +

+ +

{secondsLeft}초

+ +
+ {isLoading || isFetching ? ( +
+

QR 생성 중...

+
+ ) : qrImageUrl ? ( + 적립용 QR 코드 + ) : ( +
+

+ QR을 불러오지 못했습니다. +

+
+ )} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/apps/customer/src/app/(tabs)/mypage/_components/StatsCard.tsx b/apps/customer/src/app/(tabs)/mypage/_components/StatsCard.tsx index 39ad842..26eb765 100644 --- a/apps/customer/src/app/(tabs)/mypage/_components/StatsCard.tsx +++ b/apps/customer/src/app/(tabs)/mypage/_components/StatsCard.tsx @@ -2,15 +2,32 @@ import { useRouter } from "next/navigation"; import { Card, Icon } from "@compasser/design-system"; -import { stats } from "../_constants/stats"; -export const StatsCard = () => { +interface StatsCardProps { + totalStampCount?: number; + totalUnboxingCount?: number; + totalCouponCount?: number; + isLoading?: boolean; +} + +export const StatsCard = ({ + totalStampCount = 0, + totalUnboxingCount = 0, + totalCouponCount = 0, + isLoading = false, +}: StatsCardProps) => { const router = useRouter(); const handleMoveDetailPage = () => { router.push("/mypage/detail"); }; + const stats = [ + { label: "총 스탬프", value: totalStampCount }, + { label: "총 언박싱", value: totalUnboxingCount }, + { label: "총 쿠폰", value: totalCouponCount }, + ]; + return (
@@ -43,7 +60,7 @@ export const StatsCard = () => { {item.label}

- {item.value} + {isLoading ? "-" : item.value}

))} diff --git a/apps/customer/src/app/(tabs)/mypage/page.tsx b/apps/customer/src/app/(tabs)/mypage/page.tsx index 1ca90a6..c791319 100644 --- a/apps/customer/src/app/(tabs)/mypage/page.tsx +++ b/apps/customer/src/app/(tabs)/mypage/page.tsx @@ -3,16 +3,54 @@ import { AccountCard } from "./_components/AccountCard"; import { ProfileSection } from "./_components/ProfileSection"; import { StatsCard } from "./_components/StatsCard"; +import { useMyPageQuery } from "@/shared/queries/query/member/useMyPageQuery"; export default function MyPage() { + const { data, isLoading, isError } = useMyPageQuery(); + + if (isLoading) { + return ( +
+ +
+
+ + +
+
+
+ ); + } + + if (isError || !data) { + return ( +
+
+

+ 마이페이지 정보를 불러오지 못했습니다. +

+
+
+ ); + } + return (
- +
- +
diff --git a/apps/customer/src/shared/api/api.ts b/apps/customer/src/shared/api/api.ts index ee646dd..21cbacd 100644 --- a/apps/customer/src/shared/api/api.ts +++ b/apps/customer/src/shared/api/api.ts @@ -6,6 +6,7 @@ import { createStoreModule, type TokenPair, type TokenStore, + createMemberModule, } from "@compasser/api"; const tokenStore: TokenStore = { @@ -38,4 +39,5 @@ export const compasserApi = createCompasserApi({ }); export const authModule = createAuthModule(compasserApi); +export const memberModule = createMemberModule(compasserApi); export const storeModule = createStoreModule(compasserApi); \ No newline at end of file diff --git a/apps/customer/src/shared/queries/mutation/auth/useLogoutMutation.ts b/apps/customer/src/shared/queries/mutation/auth/useLogoutMutation.ts new file mode 100644 index 0000000..a3b4d02 --- /dev/null +++ b/apps/customer/src/shared/queries/mutation/auth/useLogoutMutation.ts @@ -0,0 +1,10 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { authModule } from "@/shared/api/api"; + +export const useLogoutMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(authModule.mutations.logout(queryClient)); +}; \ No newline at end of file diff --git a/apps/customer/src/shared/queries/query/member/useMyPageQuery.ts b/apps/customer/src/shared/queries/query/member/useMyPageQuery.ts new file mode 100644 index 0000000..566919d --- /dev/null +++ b/apps/customer/src/shared/queries/query/member/useMyPageQuery.ts @@ -0,0 +1,8 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { memberModule } from "@/shared/api/api"; + +export const useMyPageQuery = () => { + return useQuery(memberModule.queries.myPage()); +}; \ No newline at end of file diff --git a/apps/customer/src/shared/queries/query/member/useRewardQrQuery.ts b/apps/customer/src/shared/queries/query/member/useRewardQrQuery.ts new file mode 100644 index 0000000..92a270c --- /dev/null +++ b/apps/customer/src/shared/queries/query/member/useRewardQrQuery.ts @@ -0,0 +1,16 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { memberModule } from "@/shared/api/api"; + +interface UseRewardQrQueryParams { + enabled: boolean; +} + +export const useRewardQrQuery = ({ enabled }: UseRewardQrQueryParams) => { + return useQuery({ + ...memberModule.queries.qrTest(), + enabled, + refetchInterval: enabled ? 60000 : false, + }); +}; \ No newline at end of file