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

[FE] issue133, 173: 스터디 가입 기능 구현 및 스터디원에 스터디장 추가 #174

Merged
merged 11 commits into from
Jul 31, 2022
8 changes: 8 additions & 0 deletions frontend/src/api/postJoiningStudy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axiosInstance from '@api/axiosInstance';

const postNewStudy = async (studyId: number) => {
const response = await axiosInstance.post(`/api/studies/${studyId}`);
return response;
Copy link
Collaborator

Choose a reason for hiding this comment

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

response.data를 return하는건 어떨까요?
body에 errorMessage가 들어있을 수 있으니까요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아 이거 postNewStudy랑 맞춘건데 둘 다 고치겠습니다ㅎㅎ
생각해보니 errorMessage가 있었네요

};

export default postNewStudy;
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,16 @@ export const DropDownBox = styled.div<Pick<DropDownBoxProps, 'top' | 'bottom' |
border-radius: 5px;
background-color: ${theme.colors.secondary.light};
z-index: 3;

transform-origin: top;
animation: slidein 0.1s ease;
@keyframes slidein {
Copy link
Collaborator

Choose a reason for hiding this comment

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

아래로 스윽 내려오는거니까 slideDown이 어떨까요?

0% {
transform: scale(1, 0);
}
100% {
transform: scale(1, 1);
}
}
`}
`;
5 changes: 5 additions & 0 deletions frontend/src/mocks/detailStudyHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const detailStudyHandlers = [

return res(ctx.status(200), ctx.json(study));
}),
rest.post('/api/studies/:studyId', (req, res, ctx) => {
const studyId = req.params.studyId;

return res(ctx.status(200));
}),
rest.get('/api/studies/:studyId/reviews', (req, res, ctx) => {
const size = req.url.searchParams.get('size');
if (size) {
Expand Down
102 changes: 51 additions & 51 deletions frontend/src/mocks/studies.json

Large diffs are not rendered by default.

40 changes: 34 additions & 6 deletions frontend/src/pages/detail-page/DetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useParams } from 'react-router-dom';
import { AxiosResponse } from 'axios';
import { useContext } from 'react';
import { useMutation } from 'react-query';
import { Navigate, useParams } from 'react-router-dom';

import { PATH } from '@constants';

import { changeDateSeperator } from '@utils/dates';

import postJoiningStudy from '@api/postJoiningStudy';

import { LoginContext } from '@context/login/LoginProvider';

import StudyMemberSection from '@pages/detail-page/components/study-member-section/StudyMemberSection';
import StudyWideFloatBox from '@pages/detail-page/components/study-wide-float-box/StudyWideFloatBox';

Expand All @@ -18,12 +27,33 @@ import useFetchDetail from '@detail-page/hooks/useFetchDetail';
const DetailPage = () => {
const { studyId } = useParams() as { studyId: string };

const { isLoggedIn } = useContext(LoginContext);
Copy link
Collaborator

Choose a reason for hiding this comment

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

useAuth 훅이 이미 있으니, 이 훅을 조금 개선해서 사용하는건 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

좋습니당


const studyDetailQueryResult = useFetchDetail(Number(studyId));
const { mutate } = useMutation<AxiosResponse, Error, number>(postJoiningStudy);
Copy link
Collaborator

Choose a reason for hiding this comment

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

useFetchDetail처럼 한번 감싸는 hook을 만드는건 어떨까요?
예를들면, usePostJoiningStudy 이런 식으로요.
import문도 줄이고, 코드도 더 부드럽게 읽힐것 같아서요 :D

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이건 나중에 같이 리팩토링할 때 얘기하면 좋을 것 같아서 남겨놨습니다. useQuery나 useMutation 한 줄만 커스텀 훅으로 감싼 경우가 많은데 해당 페이지에서 필요한 로직들도 같이 넣어두면 더 좋을 것 같아서요! 이왕 묶을거라면 아예 useDetailPage 훅으로 묶는 게 좋을 것 같아요.


const handleRegisterBtnClick = () => {
if (!isLoggedIn) {
alert('로그인이 필요합니다.');
return;
}

const handleRegisterBtnClick = (studyId: number) => () => {
alert('아직 준비중입니다 :D');
mutate(Number(studyId), {
onError: () => {
alert('가입에 실패했습니다.');
},
onSuccess: () => {
alert('가입했습니다 :D');
studyDetailQueryResult.refetch();
},
});
};

if (!studyId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 로직이 login상태 체크보다 위로 보내는건 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

const { isLoggedIn } = useContext(LoginContext);
이거보다 위에를 말하는 건가요??

  const handleRegisterBtnClick = () => {
    if (!isLoggedIn) {
      alert('로그인이 필요합니다.');
      return;
    }
   ...

이 로직은 버튼 클릭 핸들러라서 스터디 아이디가 우선 확인되긴 합니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

아 제가 depth를 잘못 봤네요.
handleRegisterBtnClick안에 if (!studyId) {가 있는줄 알았습니다 :D

alert('잘못된 접근입니다.');
return <Navigate to={PATH.MAIN} replace={true} />;
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

}

if (studyDetailQueryResult.isFetching) return <div>Loading...</div>;

if (!studyDetailQueryResult.data) return <div>No Data</div>;
Expand Down Expand Up @@ -62,12 +92,11 @@ const DetailPage = () => {
<MarkdownRender markdownContent={description} />
</S.MarkDownContainer>
<Divider space={2} />
<StudyMemberSection members={members} />
<StudyMemberSection owner={owner} members={members} />
</S.MainDescription>
<S.FloatButtonContainer>
<S.StickyContainer>
<StudyFloatBox
studyId={id}
ownerName={owner.username}
currentMemberCount={currentMemberCount}
maxMemberCount={maxMemberCount}
Expand All @@ -82,7 +111,6 @@ const DetailPage = () => {
<StudyReviewSection studyId={id} />
<S.FixedBottomContainer>
<StudyWideFloatBox
studyId={id}
currentMemberCount={currentMemberCount}
maxMemberCount={maxMemberCount}
enrollmentEndDate={enrollmentEndDate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@ import Button from '@components/button/Button';

import * as S from '@detail-page/components/study-float-box/StudyFloatBox.style';

// TODO: 스터디에 가입한 사람인지 아닌지 상태도 받아야 함
export type StudyFloatBoxProps = Pick<
StudyDetail,
'enrollmentEndDate' | 'currentMemberCount' | 'maxMemberCount' | 'recruitmentStatus'
> & {
studyId: number;
ownerName: string;
handleRegisterBtnClick: (studyId: number) => React.MouseEventHandler<HTMLButtonElement>;
handleRegisterBtnClick: React.MouseEventHandler<HTMLButtonElement>;
};

const StudyFloatBox: React.FC<StudyFloatBoxProps> = ({
studyId,
enrollmentEndDate,
currentMemberCount,
maxMemberCount,
Expand Down Expand Up @@ -51,7 +48,7 @@ const StudyFloatBox: React.FC<StudyFloatBoxProps> = ({
<span>{ownerName}</span>
</S.Owner>
</S.StudyInfo>
<Button disabled={!isOpen} onClick={handleRegisterBtnClick(studyId)}>
<Button disabled={!isOpen} onClick={handleRegisterBtnClick}>
{isOpen ? '스터디 가입하기' : '모집이 마감되었습니다'}
</Button>
</S.StudyFloatBox>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';

import { mqDown } from '@utils/index';
Expand Down Expand Up @@ -32,6 +33,20 @@ export const MemberList = styled.ul`
}
`;

export const Owner = styled.li`
${({ theme }) => css`
position: relative;

& svg {
position: absolute;
top: 5px;
left: 20px;
stroke: ${theme.colors.tertiary.base};
fill: ${theme.colors.tertiary.base};
Copy link
Collaborator

Choose a reason for hiding this comment

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

fill도 필요한가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

내부 색을 노란색으로 채우려고 했습니다 :D

}
`}
`;

export const MoreButtonContainer = styled.div`
padding: 15px 0;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,101 @@
import { useState } from 'react';
import { TbCrown } from 'react-icons/tb';

import { DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT } from '@constants';

import { changeDateSeperator } from '@utils/dates';

import { Member } from '@custom-types/index';
import { Member, Owner } from '@custom-types/index';

import StudyMemberCard from '@pages/detail-page/components/study-member-card/StudyMemberCard';
import * as S from '@pages/detail-page/components/study-member-section/StudyMemberSection.style';

import MoreButton from '@detail-page/components/more-button/MoreButton';

export interface StudyMemberSectionProps {
owner: Owner;
members: Array<Member>;
}

const StudyMemberSection: React.FC<StudyMemberSectionProps> = ({ members }) => {
const StudyMemberSection: React.FC<StudyMemberSectionProps> = ({ owner, members }) => {
const [showAll, setShowAll] = useState<boolean>(false);

const totalMembers = [owner, ...members];

const handleShowMoreBtnClick = () => {
setShowAll(prev => !prev);
};

const renderMembers = () => {
if (totalMembers.length === 0) {
return <li>스터디원이 없습니다</li>;
}

if (showAll) {
return (
<>
<S.Owner key={owner.id}>
<a href={owner.profileUrl}>
<TbCrown size={20} />
<StudyMemberCard
username={owner.username}
imageUrl={owner.imageUrl}
startDate={changeDateSeperator('2022-07-15')}
studyCount={10}
/>
</a>
</S.Owner>
{members.map(({ id, username, imageUrl, profileUrl }) => (
<li key={id}>
<a href={profileUrl}>
<StudyMemberCard
username={username}
imageUrl={imageUrl}
startDate={changeDateSeperator('2022-07-15')}
studyCount={10}
/>
</a>
</li>
))}
</>
);
}

return (
<>
<S.Owner key={owner.id}>
<a href={owner.profileUrl}>
<TbCrown size={20} />
<StudyMemberCard
username={owner.username}
imageUrl={owner.imageUrl}
startDate={changeDateSeperator('2022-07-15')}
studyCount={10}
/>
</a>
</S.Owner>
{members.slice(0, DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT - 1).map(({ id, username, imageUrl, profileUrl }) => (
Copy link
Collaborator

Choose a reason for hiding this comment

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

DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT - 1 1을 뺀 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

스터디장을 포함해서 (더보기 누르기 전) 6명을 보여줘야하기 때문에, 스터디원은 스터디장을 제외한 5명만 필요합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

그렇군요!
스터디장이 항상 마지막에 있는걸 어떻게 확신하나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

스터디장은 스터디원 섹션에서 가장 첫 번째에 있습니다! (S.Owner 컴포넌트가 스터디장입니다.)

스터디장과 나머지 스터디원은 다른 props로 받고 있기 때문에 스터디원과 스터디장의 순서는 상관이 없습니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

아 그렇군요 :D

<li key={id}>
<a href={profileUrl}>
<StudyMemberCard
username={username}
imageUrl={imageUrl}
startDate={changeDateSeperator('2022-07-15')}
studyCount={10}
/>
</a>
</li>
))}
</>
);
};

return (
<S.StudyMemberSection>
<S.Title>
스터디원 <span>{members.length}명</span>
스터디원 <span>{totalMembers.length}명</span>
</S.Title>
<S.MemberList>
{showAll
? members.map(({ id, username, imageUrl, profileUrl }) => (
<li key={id}>
<a href={profileUrl}>
<StudyMemberCard
username={username}
imageUrl={imageUrl}
startDate={changeDateSeperator('2022-07-15')}
studyCount={10}
/>
</a>
</li>
))
: members.slice(0, DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT).map(({ id, username, imageUrl, profileUrl }) => (
<li key={id}>
<a href={profileUrl}>
<StudyMemberCard
username={username}
imageUrl={imageUrl}
startDate={changeDateSeperator('2022-07-15')}
studyCount={10}
/>
</a>
</li>
))}
</S.MemberList>
<S.MemberList>{renderMembers()}</S.MemberList>
{members.length > DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT && (
<S.MoreButtonContainer>
<MoreButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ export type StudyWideFloatBoxProps = Pick<
StudyDetail,
'enrollmentEndDate' | 'currentMemberCount' | 'maxMemberCount' | 'recruitmentStatus'
> & {
studyId: number;
handleRegisterBtnClick: (studyId: number) => React.MouseEventHandler<HTMLButtonElement>;
handleRegisterBtnClick: React.MouseEventHandler<HTMLButtonElement>;
};

const StudyWideFloatBox: React.FC<StudyWideFloatBoxProps> = ({
studyId,
enrollmentEndDate,
currentMemberCount,
maxMemberCount,
Expand Down Expand Up @@ -55,7 +53,7 @@ const StudyWideFloatBox: React.FC<StudyWideFloatBoxProps> = ({
`}
fluid={true}
disabled={!isOpen}
onClick={handleRegisterBtnClick(studyId)}
onClick={handleRegisterBtnClick}
>
{isOpen ? '가입하기' : '모집 마감'}
</Button>
Expand Down
42 changes: 20 additions & 22 deletions frontend/src/pages/login-redirect-page/LoginRedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useEffect } from 'react';
import { useMutation } from 'react-query';
import { Navigate, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';

import { PATH } from '@constants';

import type { TokenQueryData } from '@custom-types/index';

Expand All @@ -13,34 +15,30 @@ import Wrapper from '@components/wrapper/Wrapper';
const LoginRedirectPage: React.FC = () => {
const [searchParams] = useSearchParams();
const codeParam = searchParams.get('code') as string;
const navigate = useNavigate();

const { login } = useAuth();

const { data, mutate, isSuccess, isError, error } = useMutation<TokenQueryData, Error, string>(getAccessToken);

useEffect(() => {
mutate(codeParam);
}, []);
const { mutate } = useMutation<TokenQueryData, Error, string>(getAccessToken);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이것도 hook으로 감싸면 좋을것 같아요 :D

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


useEffect(() => {
if (isSuccess) {
login(data.token);
if (!codeParam) {
alert('잘못된 접근입니다.');
navigate(PATH.MAIN, { replace: true });
return;
}
}, [isSuccess]);

if (!codeParam) {
alert('잘못된 접근입니다.');
return <Navigate to="/" replace={true} />;
}

if (isError) {
alert(error.message);
return <Navigate to="/" replace={true} />;
}

if (isSuccess) {
return <Navigate to="/" replace={true} />;
}
mutate(codeParam, {
onError: error => {
alert(error.message);
Copy link
Collaborator

Choose a reason for hiding this comment

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

error.message가 있는지 확인하는 과정이 있으면 좋을것 같아요 :D

navigate(PATH.MAIN, { replace: true });
},
onSuccess: data => {
login(data.token);
navigate(PATH.MAIN, { replace: true });
},
});
}, []);

return (
<Wrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const CreateNewStudyButton = styled.button`
padding: 8px;

border-radius: 50%;
border: none;
background-color: ${theme.colors.primary.base};

&:hover {
Expand Down
Loading