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

스터디 가입 신청 기능 구현 #72

Merged
merged 10 commits into from
Nov 15, 2024
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ module.exports = {
'react/jsx-boolean-value': 'off',
'react/jsx-no-constructed-context-values': 'off',
'import/prefer-default-export': 'off',
'no-underscore-dangle': 'off'
},
}
2 changes: 1 addition & 1 deletion src/api/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { TokenReIssueResponse } from '@/types/auth';

export async function reIssueAccessToken() {
const response = await axios.get<TokenReIssueResponse>(endpoints.reIssue);
return response.data.Access_token;
return response;
}
12 changes: 12 additions & 0 deletions src/api/study/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Study,
StudyInfoResponse,
StudyMembersResponse,
StudyRoleResponse,
StudySearchRequestQuery,
StudySearchResponse,
} from '@/types/study';
Expand Down Expand Up @@ -58,6 +59,12 @@ export async function editStudyProfile(studyId: number, profile_image: FormData)
);
}

export async function applyStudyJoin(studyId: number, message: string) {
await axiosInstance.post(endpoints.applyStudyJoin(studyId), {
message,
});
}

export async function getStudyMembers(studyId: number) {
const response = await axiosInstance.get<StudyMembersResponse>(endpoints.studyMembers(studyId));
return response.data;
Expand All @@ -67,3 +74,8 @@ export async function getMyStudies() {
const response = await axiosInstance.get<Study[]>(`${endpoints.myInfo}/studies`);
return response.data;
}

export async function getMyRole(studyId: number) {
const response = await axiosInstance.get<StudyRoleResponse>(endpoints.studyRole(studyId));
return response.data.role;
}
14 changes: 12 additions & 2 deletions src/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { MemberInfoContext } from '@providers/MemberInfoProvider';
import LoginModal from '@features/modal/login/LoginModal';
import { Link } from 'react-router-dom';
import routePaths from '@constants/routePaths';
import toast from 'react-hot-toast';
import StudyCreationModal from '@/features/modal/studyCreation/StudyCreationModal';

function Header() {
Expand Down Expand Up @@ -59,7 +60,14 @@ function Header() {
>
<Button
variant="default"
onClick={() => setIsStudyCreationModalOpen(true)}
onClick={() => {
if (isLoggedIn) {
setIsStudyCreationModalOpen(true);
return;
}

toast.error('로그인이 필요한 작업입니다. 먼저 로그인해주세요.');
}}
>
스터디 생성하기
</Button>
Expand Down Expand Up @@ -165,7 +173,9 @@ function HeaderDropdown({ close, toggleContainerRef }: HeaderDropdownProps) {
return (
<div css={dropdownStyle} ref={dropdownRef}>
<ul css={menuStyle}>
<li>내 스터디</li>
<Link to={routePaths.MY_STUDY} css={{ textDecoration: 'none', color: 'black' }}>
<li>내 스터디</li>
</Link>
<li>설정</li>
<li role="presentation" onClick={() => logout()}>로그아웃</li>
</ul>
Expand Down
2 changes: 2 additions & 0 deletions src/constants/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const endpoints = {
studyInfo: (studyId: number | string) => `${prefix}/studies/${studyId}`,
editStudyProfile: (studyId: number | string) => `${prefix}/studies/${studyId}/profileImage`,
studyMembers: (studyId: number | string) => `${prefix}/studies/${studyId}/members`,
studyRole: (studyId: number | string) => `${prefix}/studies/${studyId}/members/role`,
applyStudyJoin: (studyId: number | string) => `${prefix}/studies/${studyId}/members/apply`,
submitPersonalInfo: `${prefix}/auth`,
createStudy: `${prefix}/studies`,
inviteToStudy: (studyId: number) => `${prefix}/studies/${studyId}/members/invites`,
Expand Down
1 change: 1 addition & 0 deletions src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const queryKeys = {
MAIN_SEARCH_STUDIES: 'search_studies',
STUDY_INFO: 'study_info',
STUDY_INFO_MINIFIED: 'study_info_minified',
STUDY_MEMBERS: 'study_members',
STUDY_ATTENDANCE_DATES: 'study_attendance_dates',
STUDY_ATTENDANCE_INFO: 'study_attendance_info',
Expand Down
1 change: 1 addition & 0 deletions src/constants/routePaths.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const routePaths = {
MAIN: '/',
ATTEND: '/attend',
MY_STUDY: '/mystudy',
LOGIN_SUCCESS: '/auth/kakao',
SUBMIT_PERSONAL_INFO: '/auth',
STUDY_INFO: (studyId: string | number) => `/study/${studyId}`,
Expand Down
2 changes: 1 addition & 1 deletion src/features/main/studyList/StudyGridWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function StudyGridWrapper({ studyFilter, searchKeyword }: StudyItemWrapperProps)
if (searchKeyword) {
params.name = searchKeyword;
}
if (studyFilter !== 'all') params.is_open = studyFilter === 'open';
if (studyFilter !== 'all') params.isOpen = studyFilter === 'open';
return searchStudies(params);
};
const { ref, inView } = useInView({ threshold: 1 });
Expand Down
43 changes: 39 additions & 4 deletions src/features/main/studyList/StudyItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { useStudyItemStyles } from '@features/main/studyList/StudyList.styles';
import Avatar from '@components/avatar';
import { CSSObject, useTheme } from '@emotion/react';
import UserIcon from '@assets/icons/user.svg?react';
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import routePaths from '@constants/routePaths';
import { useContext, useState } from 'react';
import ApplyStudyJoinModal from '@features/modal/invite/ApplyStudyJoinModal';
import { MemberInfoContext } from '@providers/MemberInfoProvider';
import toast from 'react-hot-toast';
import { DetailedStudyInfo } from '@/types/study';
import { getMyRole } from '@/api/study';

interface StudyItemProps {
study: DetailedStudyInfo;
Expand All @@ -19,7 +24,22 @@ function StudyItem(
}: StudyItemProps,
) {
const { containerStyle } = useStudyItemStyles();
const { isLoggedIn } = useContext(MemberInfoContext);
const [isJoinModalOpen, setIsJoinModalOpen] = useState(false);
const navigate = useNavigate();
const theme = useTheme();
const handleStudyItemClick = async () => {
if (!isLoggedIn) {
toast.error('로그인이 필요한 작업입니다. 먼저 로그인해주세요!');
return;
}
const role = await getMyRole(study.id);
if (role === '미가입') {
setIsJoinModalOpen(true);
return;
}
navigate(routePaths.STUDY_INFO(study.id));
};

const singleEllipsis: CSSObject = {
textOverflow: 'ellipsis',
Expand All @@ -35,8 +55,15 @@ function StudyItem(
};

return (
<Link to={routePaths.STUDY_INFO(study.id)} css={{ textDecoration: 'none', color: 'black' }}>
<Container direction="column" height="100%" align="flex-start" padding="20px 20px 20px 26px" cssOverride={containerStyle}>
<>
<Container
direction="column"
height="100%"
align="flex-start"
padding="20px 20px 20px 26px"
cssOverride={containerStyle}
onClick={handleStudyItemClick}
>
<Container justify="space-between" align="flex-start">
<Container
padding="8px 0"
Expand Down Expand Up @@ -78,7 +105,15 @@ function StudyItem(
</Container>
</Container>
</Container>
</Link>
{isJoinModalOpen
&& (
<ApplyStudyJoinModal
studyId={study.id}
open={isJoinModalOpen}
onClose={() => setIsJoinModalOpen(false)}
/>
)}
</>
);
}

Expand Down
1 change: 0 additions & 1 deletion src/features/main/studyList/StudyList.styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { css, useTheme } from '@emotion/react';

// eslint-disable-next-line import/prefer-default-export
export function useStudyItemStyles() {
const theme = useTheme();
const containerStyle = css`
Expand Down
91 changes: 91 additions & 0 deletions src/features/modal/invite/ApplyStudyJoinModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Modal from '@components/modal';
import StudyThumbnail from '@features/modal/invite/StudyThumbnail';
import { Heading, Paragraph } from '@components/text';
import colorTheme from '@styles/colors';
import Container from '@components/container';
import Button from '@components/button';
import theme from '@styles/theme';
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@constants/queryKeys';
import { css } from '@emotion/react';
import TextArea from '@components/textarea';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { FormErrorMessage } from '@components/text/variants';
import { applyStudyJoin, getStudyInfo } from '@/api/study';
import { ApplyJoinStudyInputs } from '@/types/study';

interface ApplyStudyJoinModalProps {
onClose: () => void;
open: boolean;
studyId: number;
}

function ApplyStudyJoinModal({ onClose, open, studyId }: ApplyStudyJoinModalProps) {
const { data: study } = useQuery({
queryKey: [queryKeys.STUDY_INFO_MINIFIED, studyId],
queryFn: () => getStudyInfo(studyId),
});
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ApplyJoinStudyInputs>();
const onSubmit = async (data: ApplyJoinStudyInputs) => {
try {
console.log(

Check warning on line 36 in src/features/modal/invite/ApplyStudyJoinModal.tsx

View workflow job for this annotation

GitHub Actions / init

Unexpected console statement
await applyStudyJoin(study?.id as number, data.message),
);
toast.success('스터디 가입 신청이 완료되었습니다!');
onClose();
} catch (e) {
toast.error('가입 신청 중 에러가 발생했습니다.');
}
};
return (
<Modal onClose={onClose} open={open}>
<form
css={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '30px', padding: '50px',
}}
onSubmit={handleSubmit(onSubmit)}
>
<StudyThumbnail src={study?.profile_image} />
<Container direction="column" gap="10px">
<Heading.H3 color={colorTheme.primary.main} weight="bolder">{study?.name}</Heading.H3>
<Paragraph variant="small" color={colorTheme.absolute.black}>
{study?.description}
</Paragraph>
</Container>
<Container direction="column" cssOverride={css`color: ${colorTheme.text.moderate}`} gap="10px">
<Paragraph variant="small" color={colorTheme.absolute.black}>
{study?.name}
에 가입 신청하기
</Paragraph>
<TextArea
placeholder="가입 신청 메시지"
css={{ width: '265px', height: '100px' }}
resize="none"
{...register('message', { required: true })}
/>
<FormErrorMessage errors={errors} name="message" />
</Container>

<Container direction="column">
<Button
variant="primary"
css={{
width: '265px',
borderRadius: theme.corners.medium,
}}
type="submit"
>
신청하기
</Button>
</Container>
</form>
</Modal>
);
}

export default ApplyStudyJoinModal;
2 changes: 1 addition & 1 deletion src/features/modal/studyCreation/StudyCreationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function StudyCreationModal({ open, onClose }: StudyCreationProps
};
formData.append('request', new Blob([JSON.stringify(requestData)], { type: 'application/json' }));
if (data.profile_image) {
formData.append('profileImage', data.profile_image);
formData.append('profile_image', data.profile_image);
} else {
const defaultImageResponse = await fetch(defaultBackground);
const blob = await defaultImageResponse.blob();
Expand Down
37 changes: 19 additions & 18 deletions src/features/myStudy/MyStudyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,36 @@
const myStudiesInfo: Study[] = await getMyStudies();
setStudies(myStudiesInfo);
} catch (e) {
console.error('과제를 불러오는 데 실패했습니다:', e);

Check warning on line 19 in src/features/myStudy/MyStudyList.tsx

View workflow job for this annotation

GitHub Actions / init

Unexpected console statement
}
};

fetchStudies();
});
}, []);

return (
<DefaultPaddedContainer css={{ boxShadow: '0 2px 2px rgba(0, 0, 0, 0.1)' }}>
<DefaultPaddedContainer css={{ boxShadow: '0 2px 2px rgba(0, 0, 0, 0.1)', backgroundColor: 'white' }}>
<Container direction="column" padding="0 10px 50px 10px">
<Container justify="flex-start" padding="15px">
<Heading.H2 css={{ margin: '20px 0', fontWeight: 'bold' }}>내 스터디</Heading.H2>
</Container>
<Grid
columns={{
initial: 1,
xs: 3,
md: 4,
lg: 5,
}}
gap={19}
>
{studies.map((study) => (
<MyStudyListItem
key={`submitted-item-${study.id}`}
study={study}
/>
))}
</Grid>
{studies.length === 0
? '가입한 스터디가 없어요. 스터디를 검색한 후 가입해보세요!'
: (
<Grid
columns={{
initial: 1,
xs: 3,
md: 4,
lg: 4,
}}
gap={15}
>
{studies.map((study) => (
<MyStudyListItem key={`submitted-item-${study.id}`} study={study} />
))}
</Grid>
)}
</Container>
</DefaultPaddedContainer>
);
Expand Down
Loading
Loading