feat: 모집공고 조회#23
Conversation
모집 공고 조회 및 부원 모집 에러 처리
There was a problem hiding this comment.
Code Review
This pull request introduces dynamic recruitment data fetching and display in the RecruitCompleteCard component, replacing hardcoded dates and contact details, and refactors the useRecruitForm hook to improve submission error handling. The review feedback suggests enhancing robustness by adding defensive type checks to date formatting utilities, preventing potential memory leaks in RecruitCompleteCard by tracking component mount status during asynchronous operations, and removing synchronous browser alert calls from the useRecruitForm hook to maintain a clean separation of concerns between business logic and UI presentation.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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)}`; | ||
| }; |
There was a problem hiding this comment.
formatKoreanDate와 createDateRangeText 함수에서 인자로 전달받는 dateText, startDate, endDate가 문자열 타입이 아니거나 예상치 못한 형식일 경우, split 메서드 호출 시 TypeError가 발생하거나 잘못된 날짜 텍스트(NaN년 NaN월 NaN일)가 생성될 수 있습니다.
방어적 프로그래밍(Defensive Programming) 관점에서 인자의 타입을 검사하고, 변환된 월/일 값이 유효한 숫자인지(Number.isNaN) 확인하는 예외 처리를 추가하여 애플리케이션의 안정성을 높이는 것을 권장합니다.
const formatKoreanDate = (dateText, shouldIncludeYear = true) => {
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 (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];
const shouldShowEndYear = startYear !== endYear;
return formatKoreanDate(startDate) + ' ~ ' + formatKoreanDate(endDate, shouldShowEndYear);
};| 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(); | ||
| }, []); |
There was a problem hiding this comment.
useEffect 내에서 비동기로 모집 공고를 조회할 때, API 요청이 완료되기 전에 사용자가 다른 페이지로 이동하여 컴포넌트가 언마운트되면, 이미 존재하지 않는 컴포넌트의 상태(setRecruitment, setHasRecruitmentError, setIsLoading)를 업데이트하려고 시도하게 됩니다.
이는 메모리 누수를 유발할 수 있으므로, 컴포넌트의 마운트 여부를 추적하는 로컬 변수(isMounted)를 활용하여 언마운트된 후에는 상태 업데이트를 건너뛰도록 개선하는 것이 안전합니다.
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 {
if (isMounted) {
setIsLoading(false);
}
}
};
fetchMainRecruitment();
return () => {
isMounted = false;
};
}, []);| if (response.data?.success === false) { | ||
| const errorMessage = response.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE; | ||
| setIsConfirmOpen(false); | ||
| setSubmitError(errorMessage); | ||
| alert(errorMessage); | ||
| return; | ||
| } |
There was a problem hiding this comment.
커스텀 훅 내부에서 브라우저의 동기식 alert 창을 직접 호출하는 것은 UI 렌더링 흐름을 일시 중단시키며, 훅의 역할(상태 및 비즈니스 로직 관리)과 UI 표현(에러 메시지 표시 방식)의 관심사 분리 원칙에 어긋납니다.
이미 이 훅은 submitError 상태를 반환하고 있으므로, alert 호출을 제거하고 이 훅을 사용하는 컴포넌트 단에서 submitError 상태를 감지하여 인라인 메시지나 커스텀 모달/토스트 UI를 통해 에러를 표현하도록 책임을 분리하는 것을 권장합니다.
if (response.data?.success === false) {
const errorMessage = response.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE;
setIsConfirmOpen(false);
setSubmitError(errorMessage);
return;
}| } catch (error) { | ||
| if (!isMountedRef.current) return; | ||
|
|
||
| setIsConfirmOpen(false); | ||
| setSubmitError( | ||
| error.response?.data?.message || '모집 신청 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.' | ||
| ); | ||
| const errorMessage = getRecruitSubmitErrorMessage(error); | ||
| setSubmitError(errorMessage); | ||
| alert(errorMessage); |
There was a problem hiding this comment.
마찬가지로 catch 블록 내에서도 브라우저의 동기식 alert를 직접 호출하기보다, submitError 상태 업데이트만 수행하고 UI 단에서 에러를 표현하도록 책임을 분리하는 것이 좋습니다.
| } catch (error) { | |
| if (!isMountedRef.current) return; | |
| setIsConfirmOpen(false); | |
| setSubmitError( | |
| error.response?.data?.message || '모집 신청 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.' | |
| ); | |
| const errorMessage = getRecruitSubmitErrorMessage(error); | |
| setSubmitError(errorMessage); | |
| alert(errorMessage); | |
| } catch (error) { | |
| if (!isMountedRef.current) return; | |
| setIsConfirmOpen(false); | |
| const errorMessage = getRecruitSubmitErrorMessage(error); | |
| setSubmitError(errorMessage); |
모집 공고 조회 및 부원 모집 에러 처리
📌 작업 내용
🔗 관련 이슈
📸 스크린샷 / 화면 녹화
🧪 테스트 방법
✅ 체크리스트
npm run lint통과를 확인했습니다npm run format을 적용했습니다console.log를 제거했습니다develop인지 확인했습니다💬 리뷰어에게 한 마디