Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 목표 생성, 목표 상세보기, 목표 삭제 후 현재 페이지 변경 로직 추가 #115

Merged
merged 9 commits into from
Jan 17, 2024
24 changes: 13 additions & 11 deletions src/app/goal/detail/saved/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@ import { useSearchParams } from 'next/navigation';
import { ContentBody, SavedFooterButton, SavedHeader, Sticker } from '@/features/goal/components';
import DetailLayout from '@/features/goal/components/detail/DetailLayout';
import { useGetGoal } from '@/hooks/reactQuery/goal';
import { usePrefetchGoals } from '@/hooks/reactQuery/goal/useGetGoals';

// TODO: 추후 GoalDetailPage와 합칠 예정
const GoalSavedPage = () => {
const id = useSearchParams().get('id');
const { data: goal } = useGetGoal({ goalId: Number(id) });
const id = Number(useSearchParams().get('id'));
const { data: goal } = useGetGoal({ goalId: id });
usePrefetchGoals();

return (
<DetailLayout
header={<SavedHeader />}
sticker={goal && <Sticker stickerUrl={goal.stickerUrl} />}
body={
goal && (
goal && (
<DetailLayout
header={<SavedHeader goalId={id} />}
sticker={<Sticker stickerUrl={goal.stickerUrl} />}
body={
<ContentBody title={goal.title} date={goal.deadline} tag={goal.tagInfo.tagContent} more={goal.description} />
)
}
footer={<SavedFooterButton />}
/>
}
footer={<SavedFooterButton goalId={id} />}
/>
)
);
};

Expand Down
11 changes: 8 additions & 3 deletions src/app/oauth2/token/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@

import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAtom } from 'jotai';
import Cookies from 'js-cookie';

import { isLoginAtom } from '@/features/auth/atom';
import { useGetMemberData } from '@/hooks/reactQuery/auth';

const OAuthTokenPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const token = searchParams.get('token');
const [isLogin, setIsLogin] = useAtom(isLoginAtom);

// const { data: memberData } = useGetMemberData({ enabled: true });
const { data: memberData } = useGetMemberData();

// TODO: useGetMemberData 가져오는 로직이 항상 token 저장 이후에 실행되도록 수정 필요.
useEffect(() => {
if (token) {
Cookies.set('accessToken', token, { secure: process.env.NODE_ENV !== 'development', expires: 7 });
setIsLogin(true);
}
}, [token]);
}, [setIsLogin, token]);

useEffect(() => {
if (memberData) {
if (isLogin && memberData) {
router.push(memberData?.nickname ? `/home/${memberData?.username}` : '/onboarding');
}
}, [memberData, router]);
}, [memberData, router, isLogin]);

return <></>;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
'use client';

import { useEffect } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';

import BandiMoori from '@/assets/images/bandi-moori.png';
import { BottomSheet, Button, Typography } from '@/components/atoms';
import { Spinner } from '@/components/atoms/spinner';
import { useDeleteGoal } from '@/hooks/reactQuery/goal';

interface DeleteGoalButtomSheetProps {
interface DeleteGoalBottomSheetProps {
open: boolean;
onClose: () => void;
goalId: number;
}

export const DeleteGoalButtomSheet = ({ open, onClose, goalId }: DeleteGoalButtomSheetProps) => {
const { mutate, isSuccess, isError } = useDeleteGoal();
const router = useRouter();

useEffect(() => {
if (isSuccess) {
onClose();
router.back();
}
}, [isSuccess, isError, onClose, router]);
export const DeleteGoalBottomSheet = ({ open, onClose, goalId }: DeleteGoalBottomSheetProps) => {
const { mutate, isPending } = useDeleteGoal();

const handleDelete = () => {
if (!goalId) {
return;
}
if (!goalId) return;

mutate({ goalId });
};
Expand All @@ -38,7 +25,7 @@ export const DeleteGoalButtomSheet = ({ open, onClose, goalId }: DeleteGoalButto
open={open}
onDismiss={onClose}
fixedMaxHeight={520}
FooterComponent={<Footer onCancel={onClose} onDelete={handleDelete} />}
FooterComponent={<Footer onCancel={onClose} onDelete={handleDelete} isPending={isPending} />}
>
<div className="h-[400px] flex flex-col items-center justify-center gap-3xs translate-y-[20px]">
<Typography type="title1" className="text-gray-70">
Expand All @@ -53,13 +40,21 @@ export const DeleteGoalButtomSheet = ({ open, onClose, goalId }: DeleteGoalButto
);
};

const Footer = ({ onCancel, onDelete }: { onCancel: VoidFunction; onDelete: VoidFunction }) => (
const Footer = ({
onCancel,
onDelete,
isPending,
}: {
onCancel: VoidFunction;
onDelete: VoidFunction;
isPending: boolean;
}) => (
<div className="flex flex-row gap-xs">
<Button variant="tertiary" onClick={onCancel}>
취소
</Button>
<Button variant="issue" onClick={onDelete}>
삭제하기
<Button variant="issue" onClick={onDelete} disabled={isPending}>
{isPending ? <Spinner /> : '삭제하기'}
</Button>
</div>
);
6 changes: 3 additions & 3 deletions src/features/goal/components/detail/DetailHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import CloseIcon from '@/assets/icons/goal/close-icon.svg';
import DeleteIcon from '@/assets/icons/goal/delete-icon.svg';
import { useGetMemberData } from '@/hooks/reactQuery/auth';

import { DeleteGoalButtomSheet } from './DeleteGoalButtomSheet';
import { DeleteGoalBottomSheet } from './DeleteGoalBottomSheet';

interface DetailHeaderProps {
goalId: number;
Expand All @@ -18,15 +18,15 @@ export const DetailHeader = ({ goalId }: DetailHeaderProps) => {

return (
<>
<Link href={{ pathname: `/home/${data?.username}` }}>
<Link href={{ pathname: `/home/${data?.username}`, query: { id: goalId } }}>
<CloseIcon />
</Link>

{goalId && (
<button
onClick={() => {
overlay.open(({ isOpen, close }) => {
return <DeleteGoalButtomSheet open={isOpen} onClose={close} goalId={goalId} />;
return <DeleteGoalBottomSheet open={isOpen} onClose={close} goalId={goalId} />;
});
}}
>
Expand Down
8 changes: 6 additions & 2 deletions src/features/goal/components/detail/SavedFooterButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import Link from 'next/link';
import { Button } from '@/components';
import { useGetMemberData } from '@/hooks/reactQuery/auth';

export const SavedFooterButton = () => {
interface SavedFooterButtonProps {
goalId: number;
}

export const SavedFooterButton = ({ goalId }: SavedFooterButtonProps) => {
const { data: memberData } = useGetMemberData();

return (
<Link href={{ pathname: `/home/${memberData?.username}` }}>
<Link href={{ pathname: `/home/${memberData?.username}`, query: { id: goalId } }}>
<Button>홈으로 가기</Button>
</Link>
);
Expand Down
11 changes: 8 additions & 3 deletions src/features/goal/components/detail/SavedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import CloseIcon from '@/assets/icons/goal/close-icon.svg';
import { Typography } from '@/components/atoms';
import { useGetMemberData } from '@/hooks/reactQuery/auth';

export const SavedHeader = () => {
interface SavedHeaderProps {
goalId: number;
}

export const SavedHeader = ({ goalId }: SavedHeaderProps) => {
const { data: memberData } = useGetMemberData();

const pathname = `/home/${memberData?.username}`;
const query = { id: goalId };

return (
<>
<Link href={{ pathname }}>
<Link href={{ pathname, query }}>
<BackIcon />
</Link>
<Typography type="header1">목표 저장 완료!</Typography>
<Link href={{ pathname }}>
<Link href={{ pathname, query }}>
<CloseIcon />
</Link>
</>
Expand Down
34 changes: 28 additions & 6 deletions src/features/home/components/lifeMap/LifeMapContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { RefObject } from 'react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { SwiperSlide } from 'swiper/react';

import StarBg from '@/app/home/[username]/startBg';
Expand All @@ -26,12 +27,21 @@ interface LifeMapProps {
isPublic?: boolean;
}

interface PositionStateProps {
position: number | null;
positionPage: number | null;
}

export const LifeMapContent = ({ goalsData, memberData, downloadSectionRef, isPublic = false }: LifeMapProps) => {
const participatedGoalsArray = partitionArrayWithSmallerFirstGroup(GOAL_COUNT_PER_PAGE, goalsData?.goals);
const LAST_PAGE = participatedGoalsArray.length;

const [position, setPosition] = useState<number | null>(null);
const [positionState, setPositionState] = useState<PositionStateProps>({
position: null,
positionPage: null,
});
const [currentPage, setCurrentPage] = useState<number | null>(null);
const paramGoalId = Number(useSearchParams().get('id'));

useEffect(() => {
if (goalsData?.goals) {
Expand All @@ -57,8 +67,19 @@ export const LifeMapContent = ({ goalsData, memberData, downloadSectionRef, isPu
default:
position = currentPosition + 1;
}
setPosition(position);
setCurrentPage(Math.floor(position / GOAL_COUNT_PER_PAGE));
const positionPage = Math.floor(position / GOAL_COUNT_PER_PAGE);
position = position % TOTAL_CURRENT_POSITIONS;
setPositionState({ position, positionPage });

let page = positionPage;
// check if query params contains id value
if (paramGoalId) {
const index = goals.findIndex(({ id: goalId }) => goalId === paramGoalId);
if (index > -1) {
page = Math.floor((index + 1) / GOAL_COUNT_PER_PAGE);
}
}
setCurrentPage(page);
};

return (
Expand Down Expand Up @@ -95,7 +116,7 @@ export const LifeMapContent = ({ goalsData, memberData, downloadSectionRef, isPu
<StarBg />
<div className="h-[520px]">
<div className="absolute inset-x-0">
<MapSwiper currentPosition={position}>
<MapSwiper currentPage={currentPage}>
{participatedGoalsArray?.map((goals, page) => (
<SwiperSlide key={`swiper-goal-${page}`}>
<div className={isPublic ? `pointer-events-none` : ''}>
Expand All @@ -104,8 +125,9 @@ export const LifeMapContent = ({ goalsData, memberData, downloadSectionRef, isPu
) : (
<MapCardPositioner type="B" goals={goals} isLast={page === LAST_PAGE - 1} />
)}
{position && currentPage === page && (
<CurrentPositionCover currentPosition={position % TOTAL_CURRENT_POSITIONS} />
{/** 현재 위치에 별 위치 시키기 위해 1) 현재 날짜가 포함된 페이지를 찾아서, 2) 포지션 위치에 별을 출력함. */}
{positionState.positionPage === page && positionState.position && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

positionState.positionPage === page && positionState.position

이거 뭘 의미하는지 한번 상수로 만들어준 다음에 넣어주실 수 있나영 ? 나중에 여기 리팩터링 제가 맡으면 눈물 한번 흘리고 시작할 거 같아서 😭

Copy link
Collaborator Author

@deepbig deepbig Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

positionSate.position 체크 로직은 뒤의 컴포넌트 출력 시 null 체크를 먼저 하려고 추가된 거에요. 별도 함수로 빼서 로직을 생성하면 결국은 앞의 positionState.positionPage === page만 남는데 이게 의미가 있을까~??

[도은 피드백 적용 후]

  const isCurrentPage = (page: number) => {
    return positionState.positionPage === page
  };

Copy link
Collaborator Author

@deepbig deepbig Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 나중에 도은이가 리팩토링 작업할 때 이해 안될 수도 있으니까 comment로 설명 적어뒀어요~~~~!

{/** 현재 위치에 별 위치 시키기 위해 1) 현재 날짜가 포함된 페이지를 찾아서, 2) 포지션 위치에 별을 출력함. */}
{positionState.positionPage === page && positionState.position && (
  <CurrentPositionCover currentPosition={positionState.position} />

<CurrentPositionCover currentPosition={positionState.position} />
)}
</div>
</SwiperSlide>
Expand Down
22 changes: 6 additions & 16 deletions src/features/home/components/mapSwiper/MapSwiper.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { type PropsWithChildren, useEffect, useState } from 'react';
import { type PropsWithChildren } from 'react';
import { Pagination } from 'swiper/modules';
import { Swiper } from 'swiper/react';

import { GOAL_COUNT_PER_PAGE } from '@/features/home/constants';

import { CustomPagination } from './CustomPagination';

import 'swiper/css';
Expand All @@ -15,22 +13,14 @@ const settings = {
modules: [Pagination],
};

interface MapSwiperProps extends PropsWithChildren {
currentPosition: number | null;
interface MapSwiperProps {
currentPage: number | null;
}

export const MapSwiper = ({ currentPosition, children }: MapSwiperProps) => {
const [initialSlide, setInitialSlide] = useState<number | null>(null);

useEffect(() => {
if (currentPosition) {
setInitialSlide(Math.floor(currentPosition / GOAL_COUNT_PER_PAGE));
}
}, [currentPosition]);

export const MapSwiper = ({ currentPage, children }: PropsWithChildren<MapSwiperProps>) => {
return (
initialSlide !== null && (
<Swiper {...settings} initialSlide={initialSlide} className="h-full">
currentPage !== null && (
<Swiper {...settings} initialSlide={currentPage} className="h-full">
{children}
<CustomPagination />
</Swiper>
Expand Down
9 changes: 5 additions & 4 deletions src/features/member/contexts/NewMemberFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ import { FormProvider, useForm } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import { DevTool } from '@hookform/devtools';

import { useCreateMemberData } from '@/hooks/reactQuery/auth';
import { useCreateMemberData, useGetMemberData } from '@/hooks/reactQuery/auth';
import { useIsMounted } from '@/hooks/useIsMounted';

import type { NewMemberFormValues } from '../types';

const NewMemberFormProvider = ({ children }: PropsWithChildren) => {
const router = useRouter();
const isMounted = useIsMounted();
const { data, mutate, isSuccess } = useCreateMemberData();
const { mutate, isSuccess } = useCreateMemberData();
const { data: memberData } = useGetMemberData();

useEffect(() => {
if (isSuccess) {
router.push(`/home/${data?.username}`);
router.push(`/home/${memberData?.username}`);
}
}, [isSuccess, router]);
}, [isSuccess, memberData, router]);

const methods = useForm<NewMemberFormValues>();

Expand Down
9 changes: 8 additions & 1 deletion src/hooks/reactQuery/goal/useDeleteGoal.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { useRouter } from 'next/navigation';
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { api } from '@/apis';
import type { MemberProps } from '@/features/member/types';

type GoalRequestParams = {
goalId: number;
};

export const useDeleteGoal = () => {
const queryClient = useQueryClient();
const router = useRouter();

return useMutation({
mutationFn: (data: GoalRequestParams) => api.delete(`/goal/${data.goalId}`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['goals'] }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['goals'] });
const memberData = queryClient.getQueryData(['memberData']) as MemberProps;
router.push(`/home/${memberData.username}`);
},
});
};
Loading