diff --git a/src/api/client/axios.ts b/src/api/client/axios.ts index d20b8ad5..d40beff0 100644 --- a/src/api/client/axios.ts +++ b/src/api/client/axios.ts @@ -59,8 +59,7 @@ const refreshAccessToken = async (): Promise => { } return null; - } catch (error) { - alert('토큰 갱신에 실패했습니다. 다시 로그인해주세요'); + } catch { window.location.href = '/login'; return null; @@ -88,6 +87,20 @@ const processFailedQueue = (error: unknown, token: string | null = null) => { failedQueue = []; }; +const hasAuthToken = (requestConfig?: InternalAxiosRequestConfig) => { + const accessToken = getCookie('accessToken'); + if (accessToken) { + return true; + } + + const authorizationHeader = requestConfig?.headers?.Authorization; + if (Array.isArray(authorizationHeader)) { + return authorizationHeader.length > 0; + } + + return Boolean(authorizationHeader); +}; + axiosInstance.interceptors.response.use( (config) => config, async (error) => { @@ -102,6 +115,11 @@ axiosInstance.interceptors.response.use( // 유효하지 않은 accessToken인 경우, 재발급 if (errorResponseBody.errorCode === 'AUTH001') { + // 로그아웃 직후/비회원 요청처럼 인증 정보가 없는 경우에는 재발급 시도를 하지 않음 + if (!hasAuthToken(originalRequest)) { + return Promise.reject(new ApiError(errorResponseBody)); + } + if (isRefreshing) { // 이미 토큰 갱신 중이면 대기열에 추가 return new Promise((resolve, reject) => { @@ -171,6 +189,11 @@ axiosInstanceForMultipart.interceptors.response.use( // 유효하지 않은 accessToken인 경우, 재발급 if (errorResponseBody.errorCode === 'AUTH001') { + // 로그아웃 직후/비회원 요청처럼 인증 정보가 없는 경우에는 재발급 시도를 하지 않음 + if (!hasAuthToken(originalRequest)) { + return Promise.reject(new ApiError(errorResponseBody)); + } + if (isRefreshing) { // 이미 토큰 갱신 중이면 대기열에 추가 return new Promise((resolve, reject) => { diff --git a/src/api/client/axiosV2.ts b/src/api/client/axiosV2.ts index 9b227e08..585c7329 100644 --- a/src/api/client/axiosV2.ts +++ b/src/api/client/axiosV2.ts @@ -59,8 +59,7 @@ const refreshAccessToken = async (): Promise => { } return null; - } catch (error) { - alert('토큰 갱신에 실패했습니다. 다시 로그인해주세요'); + } catch { window.location.href = '/login'; return null; @@ -88,6 +87,20 @@ const processFailedQueue = (error: unknown, token: string | null = null) => { failedQueue = []; }; +const hasAuthToken = (requestConfig?: InternalAxiosRequestConfig) => { + const accessToken = getCookie('accessToken'); + if (accessToken) { + return true; + } + + const authorizationHeader = requestConfig?.headers?.Authorization; + if (Array.isArray(authorizationHeader)) { + return authorizationHeader.length > 0; + } + + return Boolean(authorizationHeader); +}; + axiosInstanceV2.interceptors.response.use( (config) => config, async (error) => { @@ -102,6 +115,11 @@ axiosInstanceV2.interceptors.response.use( // 유효하지 않은 accessToken인 경우, 재발급 if (errorResponseBody.errorCode === 'AUTH001') { + // 로그아웃 직후/비회원 요청처럼 인증 정보가 없는 경우에는 재발급 시도를 하지 않음 + if (!hasAuthToken(originalRequest)) { + return Promise.reject(new ApiError(errorResponseBody)); + } + if (isRefreshing) { // 이미 토큰 갱신 중이면 대기열에 추가 return new Promise((resolve, reject) => { @@ -171,6 +189,11 @@ axiosInstanceForMultipartV2.interceptors.response.use( // 유효하지 않은 accessToken인 경우, 재발급 if (errorResponseBody.errorCode === 'AUTH001') { + // 로그아웃 직후/비회원 요청처럼 인증 정보가 없는 경우에는 재발급 시도를 하지 않음 + if (!hasAuthToken(originalRequest)) { + return Promise.reject(new ApiError(errorResponseBody)); + } + if (isRefreshing) { // 이미 토큰 갱신 중이면 대기열에 추가 return new Promise((resolve, reject) => { diff --git a/src/components/card/mission-card.tsx b/src/components/card/mission-card.tsx index 62968583..d3bdb1de 100644 --- a/src/components/card/mission-card.tsx +++ b/src/components/card/mission-card.tsx @@ -87,8 +87,12 @@ function isCardClickable( ); } - // 비리더: 진행중, 평가완료만 클릭 가능 (제출마감은 클릭 불가) - return status === 'IN_PROGRESS' || status === 'EVALUATION_COMPLETED'; + // 비리더: 진행중, 제출마감, 평가완료 시 클릭 가능 + return ( + status === 'IN_PROGRESS' || + status === 'ENDED' || + status === 'EVALUATION_COMPLETED' + ); } export default function MissionCard({ diff --git a/src/components/summary/study-info-summary.tsx b/src/components/summary/study-info-summary.tsx index 32038c08..711509cd 100644 --- a/src/components/summary/study-info-summary.tsx +++ b/src/components/summary/study-info-summary.tsx @@ -9,6 +9,7 @@ import Button from '@/components/ui/button'; import { GroupStudyFullResponse } from '@/features/study/group/api/group-study-types'; import ApplyGroupStudyModal from '@/features/study/group/ui/apply-group-study-modal'; import { useGetGroupStudyMyStatus } from '@/hooks/queries/group-study-member-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { useUserStore } from '@/stores/useUserStore'; import { EXPERIENCE_LEVEL_LABELS, @@ -28,6 +29,7 @@ export default function SummaryStudyInfo({ data }: Props) { const queryClient = useQueryClient(); const [isExpanded, setIsExpanded] = useState(false); const memberId = useUserStore((state) => state.memberId); + const showToast = useToastStore((state) => state.showToast); const { basicInfo, detailInfo, interviewPost } = data; const { @@ -119,8 +121,12 @@ export default function SummaryStudyInfo({ data }: Props) { const visibleItems = isExpanded ? infoItems : infoItems.slice(0, 4); const handleCopyURL = async () => { - await navigator.clipboard.writeText(window.location.href); - alert('스터디 링크가 복사되었습니다!'); + try { + await navigator.clipboard.writeText(window.location.href); + showToast('스터디 링크가 복사되었습니다!'); + } catch { + showToast('클립보드 복사에 실패했습니다. 다시 시도해주세요.', 'error'); + } }; const handleApplySuccess = async () => { @@ -132,11 +138,16 @@ export default function SummaryStudyInfo({ data }: Props) { } }; + // 신청 마감 여부 체크 (스터디 시작일이 오늘 이전이거나 같은 경우) + const isDeadlinePassed = + !!startDate && !dayjs(startDate).isAfter(dayjs(), 'day'); + const isApplyDisabled = isLeader || myApplicationStatus?.status !== 'NONE' || groupStudyStatus !== 'RECRUITING' || - approvedCount >= maxMembersCount; + approvedCount >= maxMembersCount || + isDeadlinePassed; const getButtonText = () => { if (myApplicationStatus?.status === 'APPROVED') { @@ -148,6 +159,9 @@ export default function SummaryStudyInfo({ data }: Props) { if (myApplicationStatus?.status === 'REJECTED') { return '신청 거절됨'; } + if (isDeadlinePassed) { + return '모집 마감'; + } return '신청하기'; };