diff --git a/package.json b/package.json index 0cc129f8..55370b7d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.1.5", "@tanstack/react-query": "^5.40.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4474328d..4f9a2d10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) @@ -2729,7 +2732,6 @@ packages: dependencies: '@types/react': 18.3.3 react: 18.3.1 - dev: false /@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} @@ -3087,6 +3089,31 @@ packages: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) '@types/react': 18.3.3 react: 18.3.1 + + /@radix-ui/react-switch@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false /@radix-ui/react-tabs@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): @@ -3242,6 +3269,33 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-use-previous@1.1.0(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-size@1.1.0(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} peerDependencies: @@ -3815,7 +3869,7 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta dependencies: '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) '@storybook/client-logger': 8.1.5 '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 diff --git a/src/app/auth/validation/complete/page.tsx b/src/app/auth/validation/complete/page.tsx index 41cb0922..987009fe 100644 --- a/src/app/auth/validation/complete/page.tsx +++ b/src/app/auth/validation/complete/page.tsx @@ -11,6 +11,7 @@ import { SIGNUP_COMPLETED } from "@auth/constants/auth"; import lottieJson from "public/assets/Problem_Complete.json"; import FewLogo from "public/enterlogo.svg"; import { Mixpanel } from "@shared/utils/mixpanel"; + export default function ValidationCompletePage() { const searchParams = useSearchParams(); const router = useRouter(); @@ -35,7 +36,6 @@ export default function ValidationCompletePage() { Mixpanel.identify({ id: memberEmail }); Mixpanel.people.set({ peoples: { $email: memberEmail } }); } - router.push("/"); }} > diff --git a/src/app/page.tsx b/src/app/page.tsx index dc85096c..10130494 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,14 @@ -import TopButton from "@common/components/TopButton"; +import { Separator } from "@shared/components/ui/separator"; + import ArticleCardsWrapper from "@main/components/ArticleCardsWrapper"; import MainHeader from "@main/components/MainHeader"; import WorkbookCardsWrapper from "@main/components/WorkbookCardsWrapper"; -import { Separator } from "@shared/components/ui/separator"; + +import TopButton from "@common/components/TopButton"; export default function MainPage() { return ( -
+
diff --git a/src/article/components/ArticleTitle/index.tsx b/src/article/components/ArticleTitle/index.tsx index 01bb04dd..a0f30ce7 100644 --- a/src/article/components/ArticleTitle/index.tsx +++ b/src/article/components/ArticleTitle/index.tsx @@ -34,7 +34,7 @@ export default function ArticleTitle() { { ...getArticleQueryOptions({ articleId }), enabled: !workbookId, - // staleTime: 1000, + }, { ...getArticleWithWorkbookQueryOptions({ @@ -42,7 +42,7 @@ export default function ArticleTitle() { articleId, }), enabled: Boolean(workbookId), - // staleTime: 1000, + }, ], }); diff --git a/src/auth/hooks/useLogout.tsx b/src/auth/hooks/useLogout.tsx index 811eee93..6e976b7b 100644 --- a/src/auth/hooks/useLogout.tsx +++ b/src/auth/hooks/useLogout.tsx @@ -11,6 +11,7 @@ import { ApiResponse } from "@api/fewFetch"; import { COOKIES } from "@shared/constants/token"; import { logOutMutaionOption } from "@auth/remotes/logoutMembersQueryOption"; +import { Mixpanel } from "@shared/utils/mixpanel"; export const useLogout = () => { const router = useRouter(); @@ -22,7 +23,7 @@ export const useLogout = () => { // 쿠키 삭제 및 로그인 페이지로 이동 deleteCookie(COOKIES.REFRESH_TOKEN); deleteCookie(COOKIES.ACCESS_TOKEN); - // Mixpanel.reset(); + Mixpanel.reset(); router.push("/"); window.location.reload(); } diff --git a/src/common/components/TopButton/index.tsx b/src/common/components/TopButton/index.tsx index dc95b89c..595dfb89 100644 --- a/src/common/components/TopButton/index.tsx +++ b/src/common/components/TopButton/index.tsx @@ -29,21 +29,23 @@ export default function TopButton() { }, []); return ( - showButton && ( -
- -
- ) + <> + {showButton && ( +
+ +
+ )} + ); } diff --git a/src/common/hooks/useSusbscribeWorkbook.tsx b/src/common/hooks/useSusbscribeWorkbook.tsx index d5da55a1..3d03d171 100644 --- a/src/common/hooks/useSusbscribeWorkbook.tsx +++ b/src/common/hooks/useSusbscribeWorkbook.tsx @@ -1,10 +1,12 @@ import { toast } from "@shared/components/ui/use-toast"; import { SUBSCRIBE_USER_ACTIONS } from "@subscription/constants/subscribe"; -import { subscribeWorkbookOptions } from "@subscription/remotes/postSubscriptionQueryOptions"; +import { subscribeWorkbookQueryOptions } from "@subscription/remotes/postSubscriptionQueryOptions"; import { useMutation } from "@tanstack/react-query"; export default function useSusbscribeWorkbook() { - const { mutate: subscribeWorkbook } = useMutation(subscribeWorkbookOptions()); + const { mutate: subscribeWorkbook } = useMutation( + subscribeWorkbookQueryOptions(), + ); const postSubscribeWorkbook = ({ workbookId, handleSucess, diff --git a/src/main/components/DropDownMenuItemList/index.tsx b/src/main/components/DropDownMenuItemList/index.tsx index dd31f514..f8508325 100644 --- a/src/main/components/DropDownMenuItemList/index.tsx +++ b/src/main/components/DropDownMenuItemList/index.tsx @@ -19,15 +19,16 @@ export function DropDownMenuItemList() { const MENU_ITEM_LIST = isLogin ? AUTH_LINK : UNAUTH_LINK; const lastIdx = MENU_ITEM_LIST.length - 1; - return ( -
    +
      {MENU_ITEM_LIST.map(({ title, component }, idx) => (
    • diff --git a/src/main/components/DropdownMenuWrapper/index.tsx b/src/main/components/DropdownMenuWrapper/index.tsx index 2d68b3c4..5673eff8 100644 --- a/src/main/components/DropdownMenuWrapper/index.tsx +++ b/src/main/components/DropdownMenuWrapper/index.tsx @@ -1,6 +1,7 @@ import { XIcon } from "lucide-react"; -import HamburgerMenu from "public/assets/icon/hamburgerMenu.svg"; + import { DropDownMenuItemList } from "../DropDownMenuItemList"; +import HamburgerMenu from "public/assets/icon/hamburgerMenu.svg"; interface DropdownMenuWrapperProps { toggleMenu: boolean; handleToggleMenu: () => void; diff --git a/src/main/components/EmailDayManagementDialog/index.tsx b/src/main/components/EmailDayManagementDialog/index.tsx new file mode 100644 index 00000000..46081564 --- /dev/null +++ b/src/main/components/EmailDayManagementDialog/index.tsx @@ -0,0 +1,85 @@ +import React, { useState } from "react"; + +import { useMutation } from "@tanstack/react-query"; + +import { Button } from "@shared/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@shared/components/ui/dialog"; +import useModalWidthControl from "@shared/hooks/useModalWidthControl"; +import { cn } from "@shared/utils/cn"; + +import { SUBSCRIPTION_EMAIL_CLIENT_INFO } from "@main/constants/emailInfo"; +import { SubscriptionManagementModel } from "@main/models/SubscriptionManagementModel"; +import { patchWorkbookEmailDayMutationOptions } from "@main/remotes/patchWorkbookEmailDayMutationOptions"; +import { SubscriptionEmailClientInfo } from "@main/types/emailInfo"; + +export default function EmailDayManagementDialog({ + day, +}: Pick) { + const [currentDay, setCurrentDay] = useState(day); + useModalWidthControl(); + + const { mutate: patchWorkbookEmailDay } = useMutation({ + ...patchWorkbookEmailDayMutationOptions(), + }); + + const onClickUpdateWorkbookEmailDay = () => { + patchWorkbookEmailDay({ + date: SubscriptionManagementModel.getDayPostInfo({ day: currentDay }), + }); + }; + + return ( + + + {SUBSCRIPTION_EMAIL_CLIENT_INFO.DAY[currentDay]} + + + + + 이메일을 받고 싶은 요일을 +
      선택해주세요. +
      + + {Object.keys(SUBSCRIPTION_EMAIL_CLIENT_INFO.DAY).map((key) => { + const currentKey = + key as keyof typeof SUBSCRIPTION_EMAIL_CLIENT_INFO.DAY; + const value = SUBSCRIPTION_EMAIL_CLIENT_INFO.DAY[currentKey]; + return ( + + ); + })} + +
      + + + 완료 + + +
      +
      + ); +} diff --git a/src/main/components/EmailManagementMenu/EmailManagementMenu.test.tsx b/src/main/components/EmailManagementMenu/EmailManagementMenu.test.tsx new file mode 100644 index 00000000..8f6c996c --- /dev/null +++ b/src/main/components/EmailManagementMenu/EmailManagementMenu.test.tsx @@ -0,0 +1,103 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; + +import { beforeEach, describe, expect, it } from "vitest"; + +import QueryClientProviders from "@shared/components/queryClientProvider"; +import { createQueryProviderWrapper } from "@shared/constants/createQueryProvider"; + +import { getSubscriptionWorkbooksQueryOptions } from "@main/remotes/getSubscriptionWorkbooksQueryOptions"; +import { patchWorkbookEmailDayMutationOptions } from "@main/remotes/patchWorkbookEmailDayMutationOptions"; +import { patchWorkbookEmailTimeMutationOptions } from "@main/remotes/patchWorkbookEmailTimeMutationOptions"; + +import SubscriptionEmailManagement from "."; +import { render, renderHook, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +describe("이메일 관련 구독 관리 컴포넌트 테스트", () => { + const renderWithClient = () => { + return render( + + + , + ); + }; + + beforeEach(async () => { + renderWithClient(); + const { result } = renderHook( + () => + useQuery({ + ...getSubscriptionWorkbooksQueryOptions({ pageType: "myPage" }), + }), + { wrapper: createQueryProviderWrapper() }, + ); + await waitFor(async () => result.current.isLoading); + await waitFor(async () => result.current.isSuccess); + }); + + it("이메일 시간 변경하기 테스트", async () => { + const { result } = renderHook( + () => + useMutation({ + ...patchWorkbookEmailTimeMutationOptions(), + }), + { wrapper: createQueryProviderWrapper() }, + ); + + const user = userEvent.setup(); + const emialTimeButton = screen.getByRole("button", { name: "오전 9시" }); + + await user.click(emialTimeButton); + const popupHeading = screen.getByRole("heading", { + level: 2, + }); + + expect(popupHeading).toHaveTextContent("아침에 이메일을 받고 싶은 시간을"); + + const tenTimeButton = screen.getByRole("button", { name: "10시" }); + await user.click(tenTimeButton); + + const closeButton = screen.getByRole("button", { name: "완료" }); + await user.click(closeButton); + + result.current.mutate({ time: "10:00" }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + + expect(screen.getByRole("button", { name: "오전 10시" })); + }); + + it("이메일 요일 변경하기 테스트", async () => { + const { result } = renderHook( + () => + useMutation({ + ...patchWorkbookEmailDayMutationOptions(), + }), + { wrapper: createQueryProviderWrapper() }, + ); + + const user = userEvent.setup(); + const emialDayButton = screen.getByRole("button", { + name: "매일 받을래요", + }); + + await user.click(emialDayButton); + const popupHeading = screen.getByRole("heading", { + level: 2, + }); + + expect(popupHeading).toHaveTextContent("이메일을 받고 싶은 요일을"); + + const notWeekButton = screen.getByRole("button", { + name: "주말에는 안 받을래요", + }); + await user.click(notWeekButton); + + const closeButton = screen.getByRole("button", { name: "완료" }); + await user.click(closeButton); + + result.current.mutate({ date: "0011111" }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + + expect(screen.getByRole("button", { name: "주말에는 안 받을래요" })); + }); +}); diff --git a/src/main/components/EmailManagementMenu/index.tsx b/src/main/components/EmailManagementMenu/index.tsx new file mode 100644 index 00000000..a1dc8b36 --- /dev/null +++ b/src/main/components/EmailManagementMenu/index.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +import { useQuery } from "@tanstack/react-query"; + +import { SubscriptionManagementModel } from "@main/models/SubscriptionManagementModel"; +import { getSubscriptionWorkbooksQueryOptions } from "@main/remotes/getSubscriptionWorkbooksQueryOptions"; + +import EmailDayManagementDialog from "../EmailDayManagementDialog"; +import EmailTimeManagementDialog from "../EmailTimeManagementDialog"; + +export default function SubscriptionEmailManagement() { + const { data } = useQuery({ + ...getSubscriptionWorkbooksQueryOptions({ pageType: "myPage" }), + select: ({ data }) => { + const subscriptionManagementModel = new SubscriptionManagementModel({ + initSubscriptionManagementServerList: data.data.workbooks, + }); + return subscriptionManagementModel.SubscriptionEmailManagementClientInfo; + }, + }); + + return ( +
      + {data && ( + <> +
      +

      이메일 받는 시간

      + +
      +
      +

      이메일 받는 요일

      + +
      + + )} +
      + ); +} diff --git a/src/main/components/EmailTimeManagementDialog/index.tsx b/src/main/components/EmailTimeManagementDialog/index.tsx new file mode 100644 index 00000000..1ed8f5fe --- /dev/null +++ b/src/main/components/EmailTimeManagementDialog/index.tsx @@ -0,0 +1,87 @@ +"use client"; +import React, { useState } from "react"; + +import { useMutation } from "@tanstack/react-query"; + +import { Button } from "@shared/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@shared/components/ui/dialog"; +import useModalWidthControl from "@shared/hooks/useModalWidthControl"; +import { cn } from "@shared/utils/cn"; + +import { SUBSCRIPTION_EMAIL_CLIENT_INFO } from "@main/constants/emailInfo"; +import { SubscriptionManagementModel } from "@main/models/SubscriptionManagementModel"; +import { patchWorkbookEmailTimeMutationOptions } from "@main/remotes/patchWorkbookEmailTimeMutationOptions"; +import { SubscriptionEmailClientInfo } from "@main/types/emailInfo"; + +export default function EmailTimeManagementDialog({ + time, +}: Pick) { + const [currentTime, setCurrentTime] = useState(time); + useModalWidthControl(); + + const { mutate: putWorkbookEmailTime } = useMutation({ + ...patchWorkbookEmailTimeMutationOptions(), + }); + + const onClickUpdateWorkbookEmailTime = () => { + putWorkbookEmailTime({ + time: SubscriptionManagementModel.getTimePostInfo({ time: currentTime }), + }); + }; + return ( + + + 오전 {SUBSCRIPTION_EMAIL_CLIENT_INFO.TIME[currentTime]}시 + + + + + 아침에 이메일을 받고 싶은 시간을 +
      선택해주세요. +
      + + {Object.keys(SUBSCRIPTION_EMAIL_CLIENT_INFO.TIME) + .sort((a, b) => Number(a) - Number(b)) + .map((key) => { + const currentKey = + key as keyof typeof SUBSCRIPTION_EMAIL_CLIENT_INFO.TIME; + const value = SUBSCRIPTION_EMAIL_CLIENT_INFO.TIME[currentKey]; + return ( + + ); + })} + +
      + + + 완료 + + +
      +
      + ); +} diff --git a/src/main/components/MainContentWrapper/index.tsx b/src/main/components/MainContentWrapper/index.tsx index d841bea1..aead4fe4 100644 --- a/src/main/components/MainContentWrapper/index.tsx +++ b/src/main/components/MainContentWrapper/index.tsx @@ -1,6 +1,7 @@ -import { cn } from "@shared/utils/cn"; import { HTMLAttributes } from "react"; +import { cn } from "@shared/utils/cn"; + interface MainContentWrapperProps extends HTMLAttributes {} export default function MainContentWrapper({ title, diff --git a/src/main/components/MainHeader/index.tsx b/src/main/components/MainHeader/index.tsx index 57f722b8..0eb5b834 100644 --- a/src/main/components/MainHeader/index.tsx +++ b/src/main/components/MainHeader/index.tsx @@ -3,26 +3,38 @@ import { useState } from "react"; import { cn } from "@shared/utils/cn"; -import FewLogo from "public/assets/icon/fewlogo.svg"; import DropDownMenuWrapper from "../DropdownMenuWrapper"; + +import FewLogo from "public/assets/icon/fewlogo.svg"; import { EVENT_NAME } from "@shared/constants/mixpanel"; import useTrackMixpanel from "@shared/hooks/useTrackMixpanel"; + import { Mixpanel } from "@shared/utils/mixpanel"; + export default function MainHeader() { useTrackMixpanel({ eventKey: EVENT_NAME.MAIN_APPEAR }); const [toggleMenu, setToggleMenu] = useState(false); const handleToggleMenu = () => { setToggleMenu((prev) => !prev); + + + if (!toggleMenu) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; + } + Mixpanel.track({ name: EVENT_NAME.MAIN_MYPAGE_TAPPED, }); + }; return (
      diff --git a/src/main/components/SubscriptionManagementDayInfo/index.tsx b/src/main/components/SubscriptionManagementDayInfo/index.tsx new file mode 100644 index 00000000..f605e7c0 --- /dev/null +++ b/src/main/components/SubscriptionManagementDayInfo/index.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { SubscriptionManagementClientInfo } from "@main/types/workbook"; + +interface SubscriptionManagementDayInfoProps + extends Pick { + workbookTitle: string | undefined; +} +export default function SubscriptionManagementDayInfo({ + dayInfo: { totalDay, currentDay }, + workbookTitle, +}: SubscriptionManagementDayInfoProps) { + return ( +
      +

      + Day{currentDay}/ + {totalDay} +

      +

      + {workbookTitle} +

      +
      + ); +} diff --git a/src/main/components/SubscriptionManagementItem/index.tsx b/src/main/components/SubscriptionManagementItem/index.tsx new file mode 100644 index 00000000..c2e6be47 --- /dev/null +++ b/src/main/components/SubscriptionManagementItem/index.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; + +import { useMutation, useQuery } from "@tanstack/react-query"; + +import { Switch } from "@shared/components/ui/switch"; +import { cn } from "@shared/utils/cn"; + +import { getWorkbookQueryOptions } from "@workbook/remotes/getWorkbookQueryOptions"; + +import { subscribeWorkbookQueryOptions } from "@subscription/remotes/postSubscriptionQueryOptions"; +import { postUnsubscriptionWorkbookMutationOptions } from "@subscription/remotes/postUnsubscriptionWorkbookMutationOptions"; + +import { SubscriptionManagementClientInfo } from "@main/types/workbook"; + +import SubscriptionManagementDayInfo from "../SubscriptionManagementDayInfo"; + +interface SubscriptionManagementItemProps + extends SubscriptionManagementClientInfo { + className: HTMLDivElement["className"]; +} +export default function SubscriptionManagementItem({ + workbookTitle, + workbookId, + isSubscription, + dayInfo, + className, +}: SubscriptionManagementItemProps) { + const [isToggleSubscription, setIsToggleSubscription] = + useState(isSubscription); + + const { mutate: unsubscriptionWorkbook } = useMutation({ + ...postUnsubscriptionWorkbookMutationOptions(), + }); + const { mutate: subscriptionWorkbook } = useMutation({ + ...subscribeWorkbookQueryOptions(), + }); + + const onClickToggleSubscription = () => { + setIsToggleSubscription((prev) => !prev); + if (isToggleSubscription) { + unsubscriptionWorkbook({ workbookId }); + } else { + subscriptionWorkbook({ workbookId }); + } + }; + + return ( +
      + + span]:h-[22px] [&>span]:w-[22px]", + )} + checked={isToggleSubscription} + onClick={onClickToggleSubscription} + /> +
      + ); +} diff --git a/src/main/components/SubscriptionManagementList/SubscriptionManagementList.test.tsx b/src/main/components/SubscriptionManagementList/SubscriptionManagementList.test.tsx new file mode 100644 index 00000000..f867959a --- /dev/null +++ b/src/main/components/SubscriptionManagementList/SubscriptionManagementList.test.tsx @@ -0,0 +1,59 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; + +import { beforeEach, describe, expect, it } from "vitest"; + +import QueryClientProviders from "@shared/components/queryClientProvider"; +import { createQueryProviderWrapper } from "@shared/constants/createQueryProvider"; + +import { postUnsubscriptionWorkbookMutationOptions } from "@subscription/remotes/postUnsubscriptionWorkbookMutationOptions"; + +import { getSubscriptionWorkbooksQueryOptions } from "@main/remotes/getSubscriptionWorkbooksQueryOptions"; + +import SubscriptionManagementList from "."; +import { render, renderHook, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +describe("워크북 구독 리스트 관리 컴포넌트 테스트", () => { + const renderWithClient = () => { + return render( + + + , + ); + }; + + beforeEach(async () => { + renderWithClient(); + const { result } = renderHook( + () => + useQuery({ + ...getSubscriptionWorkbooksQueryOptions({ pageType: "myPage" }), + }), + { wrapper: createQueryProviderWrapper() }, + ); + await waitFor(async () => result.current.isLoading); + await waitFor(async () => result.current.isSuccess); + }); + + it("심리스한 워크북 구독 테스트", async () => { + const { result: unsubscriptionresult } = renderHook( + () => + useMutation({ + ...postUnsubscriptionWorkbookMutationOptions(), + }), + { wrapper: createQueryProviderWrapper() }, + ); + screen.debug(); + + const user = userEvent.setup(); + const workbookToggleButton = screen.getByTestId("switch-2"); + expect(workbookToggleButton).toHaveValue("on"); + + await user.click(workbookToggleButton); + + unsubscriptionresult.current.mutate({ workbookId: "2" }); + await waitFor(() => + expect(unsubscriptionresult.current.isSuccess).toBeTruthy(), + ); + }); +}); diff --git a/src/main/components/SubscriptionManagementList/index.tsx b/src/main/components/SubscriptionManagementList/index.tsx new file mode 100644 index 00000000..ed1dae05 --- /dev/null +++ b/src/main/components/SubscriptionManagementList/index.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import { useQuery } from "@tanstack/react-query"; + +import { Separator } from "@shared/components/ui/separator"; +import { cn } from "@shared/utils/cn"; + +import { SubscriptionManagementModel } from "@main/models/SubscriptionManagementModel"; +import { getSubscriptionWorkbooksQueryOptions } from "@main/remotes/getSubscriptionWorkbooksQueryOptions"; + +import SubscriptionManagementItem from "../SubscriptionManagementItem"; + +export default function SubscriptionManagementList() { + const { data } = useQuery({ + ...getSubscriptionWorkbooksQueryOptions({ pageType: "myPage" }), + select: ({ data }) => { + const subscriptionManagementModel = new SubscriptionManagementModel({ + initSubscriptionManagementServerList: data.data.workbooks, + }); + return subscriptionManagementModel.SubscriptionMangementClientList; + }, + }); + + return ( +
      + + {data?.map((subscriptionInfo, idx) => ( + + ))} +
      + ); +} diff --git a/src/main/components/WorkbookCardList/index.tsx b/src/main/components/WorkbookCardList/index.tsx index 2e7bef64..d3c0a47e 100644 --- a/src/main/components/WorkbookCardList/index.tsx +++ b/src/main/components/WorkbookCardList/index.tsx @@ -20,7 +20,7 @@ export default function WorkbookCardList({ code: code !== undefined ? code : ENTIRE_CATEGORY, }), { - ...getSubscriptionWorkbooksQueryOptions(), + ...getSubscriptionWorkbooksQueryOptions({ pageType: undefined }), enabled: isLogin === true, }, ], diff --git a/src/main/components/WorkbookCardsWrapper/index.tsx b/src/main/components/WorkbookCardsWrapper/index.tsx index 358ac9b4..c61f4b64 100644 --- a/src/main/components/WorkbookCardsWrapper/index.tsx +++ b/src/main/components/WorkbookCardsWrapper/index.tsx @@ -14,7 +14,7 @@ const WorkbookCardList = dynamic(() => import("../WorkbookCardList"), { export default function WorkbookCardsWrapper() { const { category, handleCategory } = useCategory(); return ( - + ), }, + { + title: "구독 관리 제목", + component: () => ( + <> + +

      구독 관리

      + + ), + }, + + { + title: "구독 관리", + component: () => , + }, + { + title: "구독 토글 리스트", + component: () => , + }, ]; export const UNAUTH_LINK: DropdownMenuItem[] = [ { diff --git a/src/main/constants/emailInfo.ts b/src/main/constants/emailInfo.ts new file mode 100644 index 00000000..5e6ce8a4 --- /dev/null +++ b/src/main/constants/emailInfo.ts @@ -0,0 +1,40 @@ +export const SUBSCRIPTION_DAYS = { + EVERY_DAYS: "EVERY_DAYS", + WEEK_DAYS: "WEEK_DAYS", +} as const; + +export const SUBSCRIPTION_TIMES = { + "06": "06", + "07": "07", + "08": "08", + "09": "09", + "10": "10", +} as const; + +export const SUBSCRIPTION_EMAIL_CLIENT_INFO = { + TIME: { + "06": "6", + "07": "7", + "08": "8", + "09": "9", + "10": "10", + }, + DAY: { + [SUBSCRIPTION_DAYS.EVERY_DAYS]: "매일 받을래요", + [SUBSCRIPTION_DAYS.WEEK_DAYS]: "주말에는 안 받을래요", + }, +} as const; + +export const SUBSCRIPTION_EMAIL_SERVER_INFO = { + TIME: { + "06": "06:00", + "07": "07:00", + "08": "08:00", + "09": "09:00", + "10": "10:00", + }, + DAY: { + EVERY_DAYS: "1111111", + WEEK_DAYS: "0011111", + }, +} as const; diff --git a/src/main/models/SubscriptionManagementModel/SubscriptionManageMentModel.test.ts b/src/main/models/SubscriptionManagementModel/SubscriptionManageMentModel.test.ts new file mode 100644 index 00000000..b53e7977 --- /dev/null +++ b/src/main/models/SubscriptionManagementModel/SubscriptionManageMentModel.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { WorkbookSubscriptionInfo } from "@main/types/workbook"; + +import { SubscriptionManagementModel } from "."; + +const mockData: WorkbookSubscriptionInfo[] = [ + { + id: 1, + status: "ACTIVE", + totalDay: 3, + currentDay: 1, + rank: 0, + totalSubscriber: 100, + articleInfo: "{}", + subscription: { dateTimeCode: "1111111", time: "09:00" }, + }, + { + id: 2, + status: "ACTIVE", + totalDay: 3, + currentDay: 2, + rank: 0, + totalSubscriber: 1, + articleInfo: "{}", + subscription: { dateTimeCode: "1111111", time: "09:00" }, + }, + { + id: 3, + status: "DONE", + totalDay: 3, + currentDay: 3, + rank: 0, + totalSubscriber: 2, + articleInfo: "{}", + subscription: { dateTimeCode: "0011111", time: "09:00" }, + }, +]; + +describe("구독 관리 모델 테스트", () => { + let subscriptionManagementModel: SubscriptionManagementModel; + + beforeEach(() => { + subscriptionManagementModel = new SubscriptionManagementModel({ + initSubscriptionManagementServerList: mockData, + }); + }); + it("구독 관리 리스트 반환환 테스트", () => { + const subscriptionManagementClientList = + subscriptionManagementModel.SubscriptionMangementClientList; + expect(subscriptionManagementClientList).toEqual([ + expect.objectContaining({ + workbookId: "1", + isSubscription: true, + dayInfo: { totalDay: 3, currentDay: 1 }, + }), + expect.objectContaining({ + workbookId: "2", + isSubscription: true, + dayInfo: { totalDay: 3, currentDay: 2 }, + }), + expect.objectContaining({ + workbookId: "3", + isSubscription: true, + dayInfo: { totalDay: 3, currentDay: 3 }, + }), + ]); + }); + it("구독 관리 내부 이메일 상태 테스트", () => { + const subscriptionEmailManagementClientInfo = + subscriptionManagementModel.SubscriptionEmailManagementClientInfo; + expect(subscriptionEmailManagementClientInfo).toEqual({ + day: "EVERY_DAYS", + time: "09", + }); + }); +}); diff --git a/src/main/models/SubscriptionManagementModel/index.ts b/src/main/models/SubscriptionManagementModel/index.ts new file mode 100644 index 00000000..4b88535a --- /dev/null +++ b/src/main/models/SubscriptionManagementModel/index.ts @@ -0,0 +1,108 @@ +import { + SUBSCRIPTION_DAYS, + SUBSCRIPTION_EMAIL_SERVER_INFO, +} from "@main/constants/emailInfo"; +import { + SubscriptionEmailClientInfo, + SubscriptionEmailServerInfo, +} from "@main/types/emailInfo"; +import { + SubscriptionManagementClientInfo, + WorkbookSubscriptionInfo, +} from "@main/types/workbook"; + +export class SubscriptionManagementModel { + constructor({ + initSubscriptionManagementServerList, + }: { + initSubscriptionManagementServerList: WorkbookSubscriptionInfo[]; + }) { + this.subscriptionManagementServerList = + initSubscriptionManagementServerList; + } + + get SubscriptionMangementClientList(): SubscriptionManagementClientInfo[] { + return this.subscriptionManagementServerList.map((subscriptionInfo) => ({ + workbookTitle: this.getWorkbookTitle({ + workbookInfo: subscriptionInfo.workbookInfo, + }), + workbookId: subscriptionInfo.id.toString(), + isSubscription: true, + dayInfo: { + totalDay: subscriptionInfo.totalDay, + currentDay: subscriptionInfo.currentDay, + }, + })); + } + + get SubscriptionEmailManagementClientInfo(): SubscriptionEmailClientInfo { + const subscriptionServerInfo = + this.subscriptionManagementServerList[0].subscription; + return { + day: this.getDayClientInfo({ + dateTimeCode: subscriptionServerInfo.dateTimeCode, + }), + time: this.getTimeClientInfo({ time: subscriptionServerInfo.time }), + }; + } + + static getDayPostInfo({ day }: Pick) { + switch (day) { + case "EVERY_DAYS": + return SUBSCRIPTION_EMAIL_SERVER_INFO.DAY["EVERY_DAYS"]; + case "WEEK_DAYS": + return SUBSCRIPTION_EMAIL_SERVER_INFO.DAY["WEEK_DAYS"]; + } + } + + static getTimePostInfo({ time }: Pick) { + return SUBSCRIPTION_EMAIL_SERVER_INFO.TIME[time]; + } + + private getDayClientInfo({ + dateTimeCode, + }: Pick< + SubscriptionEmailServerInfo, + "dateTimeCode" + >): SubscriptionEmailClientInfo["day"] { + switch (dateTimeCode) { + case SUBSCRIPTION_EMAIL_SERVER_INFO.DAY["EVERY_DAYS"]: + return SUBSCRIPTION_DAYS["EVERY_DAYS"]; + + default: + return SUBSCRIPTION_DAYS["WEEK_DAYS"]; + } + } + + private getTimeClientInfo({ + time, + }: Pick< + SubscriptionEmailServerInfo, + "time" + >): SubscriptionEmailClientInfo["time"] { + switch (time) { + case SUBSCRIPTION_EMAIL_SERVER_INFO.TIME["06"]: + return "06"; + case SUBSCRIPTION_EMAIL_SERVER_INFO.TIME["07"]: + return "07"; + case SUBSCRIPTION_EMAIL_SERVER_INFO.TIME["08"]: + return "08"; + case SUBSCRIPTION_EMAIL_SERVER_INFO.TIME["09"]: + return "09"; + case SUBSCRIPTION_EMAIL_SERVER_INFO.TIME["10"]: + return "10"; + } + } + + private getWorkbookTitle({ + workbookInfo, + }: Pick) { + if (workbookInfo) { + const title = JSON.parse(workbookInfo)?.title as string; + return title; + } + return ""; + } + + private subscriptionManagementServerList: WorkbookSubscriptionInfo[]; +} diff --git a/src/main/remotes/getSubscriptionWorkbooksQueryOptions.ts b/src/main/remotes/getSubscriptionWorkbooksQueryOptions.ts index 36141c35..17dbde37 100644 --- a/src/main/remotes/getSubscriptionWorkbooksQueryOptions.ts +++ b/src/main/remotes/getSubscriptionWorkbooksQueryOptions.ts @@ -5,21 +5,30 @@ import { } from "@main/types/workbook"; import { UseQueryOptions } from "@tanstack/react-query"; import { API_ROUTE, QUERY_KEY } from "."; +import { PageType } from "@shared/types/view"; -const getSubscriptionWorkbooks = (): Promise< +const getSubscriptionWorkbooks = ({ + pageType, +}: { + pageType?: PageType; +}): Promise< ApiResponse> > => { - return fewFetch().get(API_ROUTE.SUBSCRIBE_WORKBOOKS); + return fewFetch().get(API_ROUTE.SUBSCRIBE_WORKBOOKS({ pageType })); }; -export const getSubscriptionWorkbooksQueryOptions = (): UseQueryOptions< +export const getSubscriptionWorkbooksQueryOptions = ({ + pageType, +}: { + pageType?: PageType; +}): UseQueryOptions< ApiResponse>, unknown, WorkbookSubscriptionInfo[] > => { return { - queryKey: [QUERY_KEY.GET_SUBSCRIBE_WORKBOOKS], - queryFn: () => getSubscriptionWorkbooks(), + queryKey: [QUERY_KEY.GET_SUBSCRIBE_WORKBOOKS, pageType], + queryFn: () => getSubscriptionWorkbooks({ pageType }), select: (data) => data.data.data.workbooks, }; }; diff --git a/src/main/remotes/index.ts b/src/main/remotes/index.ts index 229b1e62..a4d9144f 100644 --- a/src/main/remotes/index.ts +++ b/src/main/remotes/index.ts @@ -1,17 +1,22 @@ -import { CategoryClientInfo } from "@common/types/category"; import { ArticlesInfiniteQueryParams } from "@main/types/article"; +import { CategoryClientInfo } from "@common/types/category"; +import { PageType } from "@shared/types/view"; + export const API_ROUTE = { CATEGORY: "/api/v1/workbooks/categories", WORKBOOKS_WITH_CATEGORY: ({ code }: { code: CategoryClientInfo["code"] }) => `/api/v1/workbooks?category=${code}&view=mainCard`, - SUBSCRIBE_WORKBOOKS: `/api/v1/subscriptions/workbooks`, + SUBSCRIBE_WORKBOOKS: ({ pageType }: { pageType?: PageType }) => + `/api/v1/subscriptions/workbooks${pageType ? `?view=${pageType}` : ""}`, ARTICLE_CATEGORY: "/api/v1/articles/categories", ARICLES_WITH_CATEGORY: ({ code, prevArticleId, }: ArticlesInfiniteQueryParams) => `/api/v1/articles?prevArticleId=${prevArticleId}&categoryCd=${code}`, + WORKBOOK_EMAIL_TIME: "/api/v1/subscriptions/time", + WORKBOOK_EMAIL_DAY: "/api/v1/subscriptions/day", }; export const QUERY_KEY = { @@ -20,4 +25,6 @@ export const QUERY_KEY = { GET_WORKBOOKS_WITH_CATEGORY: "get-workbooks-with-category", GET_ARTICLES_WITH_CATEGORY: "get-articles-with-category", GET_SUBSCRIBE_WORKBOOKS: "get-subscribe-workbooks", + PATCH_WORKBOOK_EMAIL_TIME: "patch-workbook-email-time", + PATCH_WORKBOOK_EMAIL_DAY: "patch-workbook-email-day", }; diff --git a/src/main/remotes/patchWorkbookEmailDayMutationOptions.ts b/src/main/remotes/patchWorkbookEmailDayMutationOptions.ts new file mode 100644 index 00000000..9de34f12 --- /dev/null +++ b/src/main/remotes/patchWorkbookEmailDayMutationOptions.ts @@ -0,0 +1,30 @@ +import { UseMutationOptions } from "@tanstack/react-query"; + +import { ApiResponse, fewFetch } from "@api/fewFetch"; + +import { MessageOnlyResponse } from "@subscription/types/subscription"; + +import { API_ROUTE, QUERY_KEY } from "."; + +const patchWorkbookEmailDay = ({ + date, +}: { + date: string; +}): Promise> => { + return fewFetch().patch(API_ROUTE.WORKBOOK_EMAIL_DAY, { + body: JSON.stringify({ date }), + }); +}; + +export const patchWorkbookEmailDayMutationOptions = (): UseMutationOptions< + ApiResponse, + Error, + { + date: string; + } +> => { + return { + mutationKey: [QUERY_KEY.PATCH_WORKBOOK_EMAIL_DAY], + mutationFn: ({ date }) => patchWorkbookEmailDay({ date }), + }; +}; diff --git a/src/main/remotes/patchWorkbookEmailTimeMutationOptions.ts b/src/main/remotes/patchWorkbookEmailTimeMutationOptions.ts new file mode 100644 index 00000000..1f7fd125 --- /dev/null +++ b/src/main/remotes/patchWorkbookEmailTimeMutationOptions.ts @@ -0,0 +1,30 @@ +import { UseMutationOptions } from "@tanstack/react-query"; + +import { ApiResponse, fewFetch } from "@api/fewFetch"; + +import { MessageOnlyResponse } from "@subscription/types/subscription"; + +import { API_ROUTE, QUERY_KEY } from "."; + +const patchWorkbookEmailTime = ({ + time, +}: { + time: string; +}): Promise> => { + return fewFetch().patch(API_ROUTE.WORKBOOK_EMAIL_TIME, { + body: JSON.stringify({ time }), + }); +}; + +export const patchWorkbookEmailTimeMutationOptions = (): UseMutationOptions< + ApiResponse, + Error, + { + time: string; + } +> => { + return { + mutationKey: [QUERY_KEY.PATCH_WORKBOOK_EMAIL_TIME], + mutationFn: ({ time }) => patchWorkbookEmailTime({ time }), + }; +}; diff --git a/src/main/types/emailInfo.ts b/src/main/types/emailInfo.ts new file mode 100644 index 00000000..b4646440 --- /dev/null +++ b/src/main/types/emailInfo.ts @@ -0,0 +1,13 @@ +import { + SUBSCRIPTION_EMAIL_CLIENT_INFO, + SUBSCRIPTION_EMAIL_SERVER_INFO, +} from "@main/constants/emailInfo"; + +export interface SubscriptionEmailClientInfo { + time: keyof typeof SUBSCRIPTION_EMAIL_CLIENT_INFO.TIME; + day: keyof typeof SUBSCRIPTION_EMAIL_CLIENT_INFO.DAY; +} +export interface SubscriptionEmailServerInfo { + time: (typeof SUBSCRIPTION_EMAIL_SERVER_INFO.TIME)[keyof typeof SUBSCRIPTION_EMAIL_SERVER_INFO.TIME]; + dateTimeCode: string; +} diff --git a/src/main/types/workbook.ts b/src/main/types/workbook.ts index ed42cd33..7c43e468 100644 --- a/src/main/types/workbook.ts +++ b/src/main/types/workbook.ts @@ -1,6 +1,9 @@ -import { WorkbookServerInfo } from "@workbook/types"; import { HTMLAttributes } from "react"; +import { WorkbookServerInfo } from "@workbook/types"; + +import { SubscriptionEmailServerInfo } from "./emailInfo"; + type SubscriptionStatus = "ACTIVE" | "DONE"; export interface WorkbookSubscriptionInfo @@ -11,8 +14,19 @@ export interface WorkbookSubscriptionInfo rank: number; totalSubscriber: number; articleInfo: string; // JSON문자열 + subscription: SubscriptionEmailServerInfo; + workbookInfo?: string; // JSON 문자열 } +export type SubscriptionManagementClientInfo = { + workbookTitle: string; + workbookId: string; + isSubscription: boolean; + dayInfo: { + totalDay: WorkbookSubscriptionInfo["totalDay"]; + currentDay: WorkbookSubscriptionInfo["currentDay"]; + }; +}; export type WorkbookCardServerInfo = { subscriberCount: number; } & Omit; @@ -20,7 +34,7 @@ export type WorkbookCardServerInfo = { export interface WorkbookCardClientInfo { id: number; mainImageUrl: string; - isPriorityImage:boolean; + isPriorityImage: boolean; metaComponent: React.ReactElement; title: string; writers: string[]; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 4cf5107d..39a38ca2 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,7 +1,6 @@ import { http, HttpResponse } from "msw"; import { apiRoutes } from "@shared/constants/apiRoutes"; -import { _3_SECOND, delay } from "@shared/utils/delay"; import response from "./response"; @@ -58,7 +57,7 @@ export const workbookHandler = http.get( } // 딜레이 적용 - await delay(_3_SECOND); + // await delay(_3_SECOND); return HttpResponse.json(response[apiRoutes.workbook]); }, @@ -69,7 +68,39 @@ export const workbooksSubscriptionHandler = http.get( return HttpResponse.json(response[apiRoutes.workbooksSubscription]); }, ); +export const workbookSubscriptionHandler = http.post( + apiRoutes.workbookSubscription, + async () => { + return HttpResponse.json(response[apiRoutes.workbookSubscription]); + }, +); +export const workbookUnsubscriptionHandler = http.post( + apiRoutes.workbookUnsubscription, + async () => { + return HttpResponse.json(response[apiRoutes.workbookUnsubscription]); + }, +); +export const workbookEmailTimeHandler = http.patch( + apiRoutes.workbookEmailTime, + async ({ request }) => { + const body = request.body; + if (body) { + return HttpResponse.json(response[apiRoutes.workbookEmailTime]); + } + }, +); + +export const workbookEmailDayHandler = http.patch( + apiRoutes.workbookEmailDay, + async ({ request }) => { + const body = request.body; + console.log(body); + if (body) { + return HttpResponse.json(response[apiRoutes.workbookEmailDay]); + } + }, +); export const articleHandler = http.get( apiRoutes.article, async ({ params }) => { @@ -151,9 +182,12 @@ export const articleCategoryHandler = http.get( }, ); -export const logoutHandler = http.delete(apiRoutes.logout, async ({ request }) => { - return HttpResponse.json(response[apiRoutes.logout]); -}); +export const logoutHandler = http.delete( + apiRoutes.logout, + async ({ request }) => { + return HttpResponse.json(response[apiRoutes.logout]); + }, +); export const handlers = [ categoryHandler, @@ -169,4 +203,8 @@ export const handlers = [ membersAuthHandler, tokenHandler, logoutHandler, + workbookSubscriptionHandler, + workbookUnsubscriptionHandler, + workbookEmailTimeHandler, + workbookEmailDayHandler, ]; diff --git a/src/mocks/response/index.ts b/src/mocks/response/index.ts index a534c1f6..9c823637 100644 --- a/src/mocks/response/index.ts +++ b/src/mocks/response/index.ts @@ -2,7 +2,7 @@ import { apiRoutes } from "@shared/constants/apiRoutes"; import article1 from "./article1.json"; import articleWithWorkbook1 from "./articleWithWorkbook1.json"; -import members from "./members.json" +import members from "./members.json"; import category from "./category.json"; import mainWorkbooksEntire from "./mainWorkbooksEntire.json"; import problems1 from "./problems1.json"; @@ -13,9 +13,10 @@ import submitAnswer1 from "./submitAnswer1.json"; import submitAnswer2 from "./submitAnswer2.json"; import submitAnswer3 from "./submitAnswer3.json"; import workbook from "./workbook.json"; -import token from "./token.json" +import token from "./token.json"; import workbooksSubscription from "./workbooksSubscription.json"; -import logout from "./logout.json" +import logout from "./logout.json"; +import workbookToggleSubscription from "./workbookToggleSubscription.json"; // eslint-disable-next-line import/no-anonymous-default-export export default { @@ -35,5 +36,9 @@ export default { [apiRoutes.category]: category, [apiRoutes.token]: token, [apiRoutes.articleCategory]: category, - [apiRoutes.logout]: logout + [apiRoutes.logout]: logout, + [apiRoutes.workbookSubscription]: workbookToggleSubscription, + [apiRoutes.workbookUnsubscription]: workbookToggleSubscription, + [apiRoutes.workbookEmailDay]: workbookToggleSubscription, + [apiRoutes.workbookEmailTime]: workbookToggleSubscription, }; diff --git a/src/mocks/response/workbookToggleSubscription.json b/src/mocks/response/workbookToggleSubscription.json new file mode 100644 index 00000000..50203e8f --- /dev/null +++ b/src/mocks/response/workbookToggleSubscription.json @@ -0,0 +1,3 @@ +{ + "message": "성공" +} diff --git a/src/mocks/response/workbooksSubscription.json b/src/mocks/response/workbooksSubscription.json index 4e7ff35d..50040d2a 100644 --- a/src/mocks/response/workbooksSubscription.json +++ b/src/mocks/response/workbooksSubscription.json @@ -8,7 +8,8 @@ "currentDay": 1, "rank": 0, "totalSubscriber": 100, - "articleInfo": "{}" + "articleInfo": "{}", + "subscription": { "date": "1111111", "time": "09:00" } }, { "id": 2, @@ -17,7 +18,8 @@ "currentDay": 2, "rank": 0, "totalSubscriber": 1, - "articleInfo": "{}" + "articleInfo": "{}", + "subscription": { "date": "1111111", "time": "09:00" } }, { "id": 3, @@ -26,7 +28,8 @@ "currentDay": 3, "rank": 0, "totalSubscriber": 2, - "articleInfo": "{}" + "articleInfo": "{}", + "subscription": { "date": "0011111", "time": "09:00" } } ] }, diff --git a/src/problem/components/ProblemTitle/index.tsx b/src/problem/components/ProblemTitle/index.tsx index 16475f1c..3b35d1ec 100644 --- a/src/problem/components/ProblemTitle/index.tsx +++ b/src/problem/components/ProblemTitle/index.tsx @@ -6,7 +6,6 @@ import { useMutationState, useQuery } from "@tanstack/react-query"; import { ApiResponse } from "@api/fewFetch"; - import { PROBLEM_TITLE_INFO } from "@problem/constants/problemInfo"; import { QUERY_KEY } from "@problem/remotes/api"; import { getProblemQueryOptions } from "@problem/remotes/getProblemQueryOptions"; diff --git a/src/shared/components/ui/switch.tsx b/src/shared/components/ui/switch.tsx new file mode 100644 index 00000000..b8b6cfe6 --- /dev/null +++ b/src/shared/components/ui/switch.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" + +import { cn } from "@shared/utils/cn" + +import * as SwitchPrimitives from "@radix-ui/react-switch" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/shared/constants/apiRoutes.ts b/src/shared/constants/apiRoutes.ts index 78848349..51ca5fe0 100644 --- a/src/shared/constants/apiRoutes.ts +++ b/src/shared/constants/apiRoutes.ts @@ -13,4 +13,8 @@ export const apiRoutes = { token: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/members/token`, articleCategory: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/articles/categories`, logout: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/members`, + workbookUnsubscription: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/workbooks/:workbookId/unsubs`, + workbookSubscription: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/workbooks/:workbookId/subs`, + workbookEmailTime: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/subscriptions/time`, + workbookEmailDay: `${process.env.NEXT_PUBLIC_FEW_WEB}/api/v1/subscriptions/date`, }; diff --git a/src/shared/hooks/useModalWidthControl.tsx b/src/shared/hooks/useModalWidthControl.tsx new file mode 100644 index 00000000..0c2af35f --- /dev/null +++ b/src/shared/hooks/useModalWidthControl.tsx @@ -0,0 +1,11 @@ +"use client"; +import { useEffect } from "react"; + +export default function useModalWidthControl() { + useEffect(function removeAttribute() { + const intervalId = setInterval(() => { + document.body.removeAttribute("data-scroll-locked"); + }, 100); + return () => clearInterval(intervalId); + }, []); +} diff --git a/src/shared/types/view.ts b/src/shared/types/view.ts new file mode 100644 index 00000000..99efc53b --- /dev/null +++ b/src/shared/types/view.ts @@ -0,0 +1 @@ +export type PageType = "myPage"; diff --git a/src/subscription/hooks/useSubscribeForm.tsx b/src/subscription/hooks/useSubscribeForm.tsx index 1afa8108..f4cc9735 100644 --- a/src/subscription/hooks/useSubscribeForm.tsx +++ b/src/subscription/hooks/useSubscribeForm.tsx @@ -8,7 +8,7 @@ import { useToast } from "@shared/components/ui/use-toast"; import useWorkbookId from "@shared/hooks/useWorkbookId"; import { SUBSCRIBE_USER_ACTIONS } from "@subscription/constants/subscribe"; -import { subscribeWorkbookOptions } from "@subscription/remotes/postSubscriptionQueryOptions"; +import { subscribeWorkbookQueryOptions } from "@subscription/remotes/postSubscriptionQueryOptions"; import { emailSubscribeSchema } from "@common/schemas/emailSchema"; import { EmailSubscribeFormData } from "@common/types/emailSubscribeData"; @@ -27,7 +27,9 @@ export const useSubscribeForm = () => { mode: "onSubmit", }); - const { mutate: subscribeWorkbook } = useMutation(subscribeWorkbookOptions()); + const { mutate: subscribeWorkbook } = useMutation( + subscribeWorkbookQueryOptions(), + ); const onSubmit = (values: EmailSubscribeFormData) => { try { diff --git a/src/subscription/remotes/api.ts b/src/subscription/remotes/api.ts index 0b9fca90..e5490250 100644 --- a/src/subscription/remotes/api.ts +++ b/src/subscription/remotes/api.ts @@ -1,8 +1,11 @@ export const API_ROUTE = { SUBSCRIBE: (workbookId: string) => `/api/v1/workbooks/${workbookId}/subs`, - UNSUBSCRIBE: () => `/api/v1/subscriptions/unsubs` + UNSUBSCRIBE: () => `/api/v1/subscriptions/unsubs`, + UNSUBSCRIBE_WORKBOOK: ({ workbookId }: { workbookId: string }) => + `/api/v1/workbooks/${workbookId}/unsubs`, }; export const QUERY_KEY = { SUBSCRIBE_WORKBOOK: "sub-workbook", - UNSUBSCRIBE_WORKBOOK: "unsub-workbook" + UNSUBSCRIBE_WORKBOOKS: "unsub-workbooks", + UNSUBSCRIBE_WORKBOOK: "unsub-workbook", }; diff --git a/src/subscription/remotes/postSubscriptionQueryOptions.ts b/src/subscription/remotes/postSubscriptionQueryOptions.ts index fe4396d2..8be6cd75 100644 --- a/src/subscription/remotes/postSubscriptionQueryOptions.ts +++ b/src/subscription/remotes/postSubscriptionQueryOptions.ts @@ -1,11 +1,12 @@ import { UseMutationOptions } from "@tanstack/react-query"; +import { ApiResponse, FewError, fewFetch } from "@api/fewFetch"; + import { MessageOnlyResponse, SubscribeParams, } from "@subscription/types/subscription"; -import { ApiResponse, FewError, fewFetch } from "@api/fewFetch"; import { API_ROUTE, QUERY_KEY } from "./api"; export const subscribeWorkbook = ({ @@ -16,7 +17,7 @@ export const subscribeWorkbook = ({ return fewFetch().post(API_ROUTE.SUBSCRIBE(workbookId)); }; -export const subscribeWorkbookOptions = (): UseMutationOptions< +export const subscribeWorkbookQueryOptions = (): UseMutationOptions< ApiResponse, ApiResponse, Pick diff --git a/src/subscription/remotes/postUnsubscriptionQueryOptions.ts b/src/subscription/remotes/postUnsubscriptionQueryOptions.ts index 002c2bb7..8162d2c3 100644 --- a/src/subscription/remotes/postUnsubscriptionQueryOptions.ts +++ b/src/subscription/remotes/postUnsubscriptionQueryOptions.ts @@ -1,11 +1,12 @@ import { UseMutationOptions } from "@tanstack/react-query"; +import { ApiResponse, fewFetch } from "@api/fewFetch"; + import { MessageOnlyResponse, UnsubscribeBody, } from "@subscription/types/subscription"; -import { ApiResponse, fewFetch } from "@api/fewFetch"; import { API_ROUTE, QUERY_KEY } from "./api"; export const unsubscribeWorkbook = ( diff --git a/src/subscription/remotes/postUnsubscriptionWorkbookMutationOptions.ts b/src/subscription/remotes/postUnsubscriptionWorkbookMutationOptions.ts new file mode 100644 index 00000000..4b2070eb --- /dev/null +++ b/src/subscription/remotes/postUnsubscriptionWorkbookMutationOptions.ts @@ -0,0 +1,26 @@ +import { UseMutationOptions } from "@tanstack/react-query"; + +import { ApiResponse, fewFetch } from "@api/fewFetch"; + +import { MessageOnlyResponse } from "@subscription/types/subscription"; + +import { SubscriptionManagementClientInfo } from "./../../main/types/workbook"; +import { API_ROUTE, QUERY_KEY } from "./api"; + +const unsubscriptionWorkbook = ({ + workbookId, +}: Pick): Promise< + ApiResponse +> => { + return fewFetch().post(API_ROUTE.UNSUBSCRIBE_WORKBOOK({ workbookId })); +}; +export const postUnsubscriptionWorkbookMutationOptions = (): UseMutationOptions< + ApiResponse, + Error, + Pick +> => { + return { + mutationKey: [QUERY_KEY.UNSUBSCRIBE_WORKBOOK], + mutationFn: ({ workbookId }) => unsubscriptionWorkbook({ workbookId }), + }; +};