-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from 7 commits
5a4741b
a253173
6a22092
7f37285
96a9c4e
7c18d74
e84f16b
dd4a855
5035b8b
4c65cf2
598c4b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
}; | ||
|
||
export default postNewStudy; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아래로 스윽 내려오는거니까 slideDown이 어떨까요? |
||
0% { | ||
transform: scale(1, 0); | ||
} | ||
100% { | ||
transform: scale(1, 1); | ||
} | ||
} | ||
`} | ||
`; |
Large diffs are not rendered by default.
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'; | ||
|
||
|
@@ -18,12 +27,33 @@ import useFetchDetail from '@detail-page/hooks/useFetchDetail'; | |
const DetailPage = () => { | ||
const { studyId } = useParams() as { studyId: string }; | ||
|
||
const { isLoggedIn } = useContext(LoginContext); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋습니당 |
||
|
||
const studyDetailQueryResult = useFetchDetail(Number(studyId)); | ||
const { mutate } = useMutation<AxiosResponse, Error, number>(postJoiningStudy); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 로직이 login상태 체크보다 위로 보내는건 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
const handleRegisterBtnClick = () => {
if (!isLoggedIn) {
alert('로그인이 필요합니다.');
return;
}
... 이 로직은 버튼 클릭 핸들러라서 스터디 아이디가 우선 확인되긴 합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 제가 depth를 잘못 봤네요. |
||
alert('잘못된 접근입니다.'); | ||
return <Navigate to={PATH.MAIN} replace={true} />; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>; | ||
|
@@ -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} | ||
|
@@ -82,7 +111,6 @@ const DetailPage = () => { | |
<StudyReviewSection studyId={id} /> | ||
<S.FixedBottomContainer> | ||
<StudyWideFloatBox | ||
studyId={id} | ||
currentMemberCount={currentMemberCount} | ||
maxMemberCount={maxMemberCount} | ||
enrollmentEndDate={enrollmentEndDate} | ||
|
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'; | ||
|
@@ -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}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fill도 필요한가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 내부 색을 노란색으로 채우려고 했습니다 :D |
||
} | ||
`} | ||
`; | ||
|
||
export const MoreButtonContainer = styled.div` | ||
padding: 15px 0; | ||
|
||
|
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 }) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스터디장을 포함해서 (더보기 누르기 전) 6명을 보여줘야하기 때문에, 스터디원은 스터디장을 제외한 5명만 필요합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그렇군요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스터디장은 스터디원 섹션에서 가장 첫 번째에 있습니다! (S.Owner 컴포넌트가 스터디장입니다.) 스터디장과 나머지 스터디원은 다른 props로 받고 있기 때문에 스터디원과 스터디장의 순서는 상관이 없습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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'; | ||
|
||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것도 hook으로 감싸면 좋을것 같아요 :D There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
There was a problem hiding this comment.
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가 들어있을 수 있으니까요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 이거 postNewStudy랑 맞춘건데 둘 다 고치겠습니다ㅎㅎ
생각해보니 errorMessage가 있었네요