From d9a031d12c502d0417fa3c97abe3052a22ae76ed Mon Sep 17 00:00:00 2001 From: kimbosung521 Date: Sun, 5 Jul 2026 15:35:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=AA=A8=EC=A7=91=EA=B3=B5?= =?UTF-8?q?=EA=B3=A0=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모집 공고 조회 및 부원 모집 에러 처리 --- src/apis/recruit.js | 6 + .../recruit/RecruitCompleteCard.jsx | 119 ++++++++++++++---- src/hooks/useRecruitForm.js | 24 +++- 3 files changed, 123 insertions(+), 26 deletions(-) diff --git a/src/apis/recruit.js b/src/apis/recruit.js index fc72434..aa2e5e1 100644 --- a/src/apis/recruit.js +++ b/src/apis/recruit.js @@ -7,6 +7,12 @@ import instance from './instance'; */ export const submitRecruit = (data) => instance.post('/api/v1/visitor/applicantMembers', data); +/** + * 신입 부원 모집 공고 조회 + * @returns {Promise} + */ +export const getMainRecruitment = () => instance.get('/api/v1/visitor/main/recruitment'); + /** * (관리자) 지원자 목록 조회 * @returns {Promise} diff --git a/src/components/recruit/RecruitCompleteCard.jsx b/src/components/recruit/RecruitCompleteCard.jsx index c81b91a..2c5b131 100644 --- a/src/components/recruit/RecruitCompleteCard.jsx +++ b/src/components/recruit/RecruitCompleteCard.jsx @@ -1,8 +1,67 @@ +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; +import { getMainRecruitment } from '@/apis/recruit'; import { ROUTES } from '@/constants/routes'; -function RecruitCompleteCard() { +const COMPLETE_MESSAGE = '회장의 연락을 통해 면접 일정이 조정될 예정이오니, 잠시 기다려 주세요.'; + +const EMPTY_RECRUITMENT_NOTICE = '모집 공고 정보를 불러오지 못했습니다.'; + +const formatKoreanDate = (dateText, shouldIncludeYear = true) => { + if (!dateText) return ''; + + const [year, month, day] = dateText.split('-'); + if (!year || !month || !day) return dateText; + + const formattedMonth = Number(month); + const formattedDay = Number(day); + const monthDayText = `${formattedMonth}월 ${formattedDay}일`; + + return shouldIncludeYear ? `${year}년 ${monthDayText}` : monthDayText; +}; + +const createDateRangeText = (startDate, endDate) => { + if (!startDate && !endDate) return ''; + if (!startDate) return formatKoreanDate(endDate); + if (!endDate || startDate === endDate) return formatKoreanDate(startDate); + + const startYear = startDate.split('-')[0]; + const endYear = endDate.split('-')[0]; + const shouldShowEndYear = startYear !== endYear; + + return `${formatKoreanDate(startDate)} ~ ${formatKoreanDate(endDate, shouldShowEndYear)}`; +}; + +const RecruitCompleteCard = () => { + const [recruitment, setRecruitment] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [hasRecruitmentError, setHasRecruitmentError] = useState(false); + + useEffect(() => { + const fetchMainRecruitment = async () => { + try { + const response = await getMainRecruitment(); + setRecruitment(response.data?.data ?? null); + setHasRecruitmentError(false); + } catch (error) { + console.error('[RecruitCompleteCard] 모집 공고 조회 실패', error); + setHasRecruitmentError(true); + } finally { + setIsLoading(false); + } + }; + + fetchMainRecruitment(); + }, []); + + const interviewPeriod = createDateRangeText( + recruitment?.interviewStart, + recruitment?.interviewEnd + ); + const notificationDate = formatKoreanDate(recruitment?.notificationDate); + const contactText = [recruitment?.bossName, recruitment?.contactNumber].filter(Boolean).join(' '); + return (
@@ -12,26 +71,42 @@ function RecruitCompleteCard() {
-

- 면접 기간: - 2026년 4월 6일 ~ 4월 7일 -

-

- 합격 통보: - 2026년 4월 7일 예정{' '} - - (해당 일정은 변동될 수 있습니다.) - -

-

- 면접 장소: - 3-511호 ONE 동아리방 -

- -

- 회장의 연락을 통해 면접 일정이 조정될 예정이오니, 잠시 기다려 주세요. -

-

문의: 회장 최예은 010-1111-1111

+ {isLoading &&

모집 공고 정보를 불러오는 중입니다.

} + + {!isLoading && hasRecruitmentError && ( +

+ {EMPTY_RECRUITMENT_NOTICE} +

+ )} + + {!isLoading && !hasRecruitmentError && recruitment && ( + <> + {interviewPeriod && ( +

+ 면접 기간: + {interviewPeriod} +

+ )} + {notificationDate && ( +

+ 합격 통보: + {notificationDate} 예정{' '} + + (해당 일정은 변동될 수 있습니다.) + +

+ )} + {recruitment.roomLocation && ( +

+ 면접 장소: + {recruitment.roomLocation} +

+ )} + + )} + +

{COMPLETE_MESSAGE}

+ {contactText &&

문의: 회장 {contactText}

}
); -} +}; export default RecruitCompleteCard; diff --git a/src/hooks/useRecruitForm.js b/src/hooks/useRecruitForm.js index f424a96..2411ced 100644 --- a/src/hooks/useRecruitForm.js +++ b/src/hooks/useRecruitForm.js @@ -18,6 +18,12 @@ const FIELD_VALUE_FORMATTERS = { const DEFAULT_FIELD_VALUE_FORMATTER = (value) => value; +const DEFAULT_SUBMIT_ERROR_MESSAGE = '모집 신청 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.'; + +const getRecruitSubmitErrorMessage = (error) => { + return error.response?.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE; +}; + export function useRecruitForm() { const navigate = useNavigate(); const isMountedRef = useRef(true); @@ -29,6 +35,8 @@ export function useRecruitForm() { const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { + isMountedRef.current = true; + return () => { isMountedRef.current = false; }; @@ -73,18 +81,26 @@ export function useRecruitForm() { setIsSubmitting(true); setSubmitError(''); try { - await submitRecruit({ ...form, grade: Number(form.grade) }); + const response = await submitRecruit({ ...form, grade: Number(form.grade) }); if (!isMountedRef.current) return; + if (response.data?.success === false) { + const errorMessage = response.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE; + setIsConfirmOpen(false); + setSubmitError(errorMessage); + alert(errorMessage); + return; + } + setIsConfirmOpen(false); navigate(ROUTES.RECRUIT_COMPLETE); } catch (error) { if (!isMountedRef.current) return; setIsConfirmOpen(false); - setSubmitError( - error.response?.data?.message || '모집 신청 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.' - ); + const errorMessage = getRecruitSubmitErrorMessage(error); + setSubmitError(errorMessage); + alert(errorMessage); } finally { if (isMountedRef.current) { setIsSubmitting(false); From f1fdd2eb3024f261c92eb7728357905b891ce5c0 Mon Sep 17 00:00:00 2001 From: kimbosung521 Date: Sun, 5 Jul 2026 15:47:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=A0=9C=EB=AF=B8=EB=82=98=EC=9D=B4?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recruit/RecruitCompleteCard.jsx | 24 +++++++++++++++---- src/hooks/useRecruitForm.js | 2 -- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/recruit/RecruitCompleteCard.jsx b/src/components/recruit/RecruitCompleteCard.jsx index 2c5b131..c0f7d4b 100644 --- a/src/components/recruit/RecruitCompleteCard.jsx +++ b/src/components/recruit/RecruitCompleteCard.jsx @@ -9,22 +9,24 @@ const COMPLETE_MESSAGE = '회장의 연락을 통해 면접 일정이 조정될 const EMPTY_RECRUITMENT_NOTICE = '모집 공고 정보를 불러오지 못했습니다.'; const formatKoreanDate = (dateText, shouldIncludeYear = true) => { - if (!dateText) return ''; + if (typeof dateText !== 'string') return ''; const [year, month, day] = dateText.split('-'); if (!year || !month || !day) return dateText; const formattedMonth = Number(month); const formattedDay = Number(day); + if (Number.isNaN(formattedMonth) || Number.isNaN(formattedDay)) return dateText; + const monthDayText = `${formattedMonth}월 ${formattedDay}일`; return shouldIncludeYear ? `${year}년 ${monthDayText}` : monthDayText; }; const createDateRangeText = (startDate, endDate) => { - if (!startDate && !endDate) return ''; - if (!startDate) return formatKoreanDate(endDate); - if (!endDate || startDate === endDate) return formatKoreanDate(startDate); + if (typeof startDate !== 'string' && typeof endDate !== 'string') return ''; + if (typeof startDate !== 'string') return formatKoreanDate(endDate); + if (typeof endDate !== 'string' || startDate === endDate) return formatKoreanDate(startDate); const startYear = startDate.split('-')[0]; const endYear = endDate.split('-')[0]; @@ -39,20 +41,32 @@ const RecruitCompleteCard = () => { const [hasRecruitmentError, setHasRecruitmentError] = useState(false); useEffect(() => { + let isMounted = true; + const fetchMainRecruitment = async () => { try { const response = await getMainRecruitment(); + if (!isMounted) return; + setRecruitment(response.data?.data ?? null); setHasRecruitmentError(false); } catch (error) { + if (!isMounted) return; + console.error('[RecruitCompleteCard] 모집 공고 조회 실패', error); setHasRecruitmentError(true); } finally { - setIsLoading(false); + if (isMounted) { + setIsLoading(false); + } } }; fetchMainRecruitment(); + + return () => { + isMounted = false; + }; }, []); const interviewPeriod = createDateRangeText( diff --git a/src/hooks/useRecruitForm.js b/src/hooks/useRecruitForm.js index 2411ced..39e340e 100644 --- a/src/hooks/useRecruitForm.js +++ b/src/hooks/useRecruitForm.js @@ -88,7 +88,6 @@ export function useRecruitForm() { const errorMessage = response.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE; setIsConfirmOpen(false); setSubmitError(errorMessage); - alert(errorMessage); return; } @@ -100,7 +99,6 @@ export function useRecruitForm() { setIsConfirmOpen(false); const errorMessage = getRecruitSubmitErrorMessage(error); setSubmitError(errorMessage); - alert(errorMessage); } finally { if (isMountedRef.current) { setIsSubmitting(false);