diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d62822e..30d1652 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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' }, } diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts index 748b46a..95a8d78 100644 --- a/src/api/auth/index.ts +++ b/src/api/auth/index.ts @@ -4,5 +4,5 @@ import { TokenReIssueResponse } from '@/types/auth'; export async function reIssueAccessToken() { const response = await axios.get(endpoints.reIssue); - return response.data.Access_token; + return response; } diff --git a/src/api/study/index.ts b/src/api/study/index.ts index 571a6ec..8805054 100644 --- a/src/api/study/index.ts +++ b/src/api/study/index.ts @@ -3,6 +3,7 @@ import type { Study, StudyInfoResponse, StudyMembersResponse, + StudyRoleResponse, StudySearchRequestQuery, StudySearchResponse, } from '@/types/study'; @@ -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(endpoints.studyMembers(studyId)); return response.data; @@ -67,3 +74,8 @@ export async function getMyStudies() { const response = await axiosInstance.get(`${endpoints.myInfo}/studies`); return response.data; } + +export async function getMyRole(studyId: number) { + const response = await axiosInstance.get(endpoints.studyRole(studyId)); + return response.data.role; +} diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index f2591bd..69d7fe9 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -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() { @@ -59,7 +60,14 @@ function Header() { > @@ -165,7 +173,9 @@ function HeaderDropdown({ close, toggleContainerRef }: HeaderDropdownProps) { return (
    -
  • 내 스터디
  • + +
  • 내 스터디
  • +
  • 설정
  • logout()}>로그아웃
diff --git a/src/constants/endpoints.ts b/src/constants/endpoints.ts index 0c39621..b49373e 100644 --- a/src/constants/endpoints.ts +++ b/src/constants/endpoints.ts @@ -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`, diff --git a/src/constants/queryKeys.ts b/src/constants/queryKeys.ts index e53d788..dbf4884 100644 --- a/src/constants/queryKeys.ts +++ b/src/constants/queryKeys.ts @@ -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', diff --git a/src/constants/routePaths.ts b/src/constants/routePaths.ts index 67fedf8..2e154d0 100644 --- a/src/constants/routePaths.ts +++ b/src/constants/routePaths.ts @@ -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}`, diff --git a/src/features/main/studyList/StudyGridWrapper.tsx b/src/features/main/studyList/StudyGridWrapper.tsx index e97cfe4..b10101c 100644 --- a/src/features/main/studyList/StudyGridWrapper.tsx +++ b/src/features/main/studyList/StudyGridWrapper.tsx @@ -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 }); diff --git a/src/features/main/studyList/StudyItem.tsx b/src/features/main/studyList/StudyItem.tsx index ecc6b0e..91cbb21 100644 --- a/src/features/main/studyList/StudyItem.tsx +++ b/src/features/main/studyList/StudyItem.tsx @@ -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; @@ -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', @@ -35,8 +55,15 @@ function StudyItem( }; return ( - - + <> + - + {isJoinModalOpen + && ( + setIsJoinModalOpen(false)} + /> + )} + ); } diff --git a/src/features/main/studyList/StudyList.styles.ts b/src/features/main/studyList/StudyList.styles.ts index c997b2e..96df782 100644 --- a/src/features/main/studyList/StudyList.styles.ts +++ b/src/features/main/studyList/StudyList.styles.ts @@ -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` diff --git a/src/features/modal/invite/ApplyStudyJoinModal.tsx b/src/features/modal/invite/ApplyStudyJoinModal.tsx new file mode 100644 index 0000000..cbb2ff1 --- /dev/null +++ b/src/features/modal/invite/ApplyStudyJoinModal.tsx @@ -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(); + const onSubmit = async (data: ApplyJoinStudyInputs) => { + try { + console.log( + await applyStudyJoin(study?.id as number, data.message), + ); + toast.success('스터디 가입 신청이 완료되었습니다!'); + onClose(); + } catch (e) { + toast.error('가입 신청 중 에러가 발생했습니다.'); + } + }; + return ( + +
+ + + {study?.name} + + {study?.description} + + + + + {study?.name} + 에 가입 신청하기 + +