Skip to content

feat: 모집공고 조회#23

Open
kimbosung521 wants to merge 2 commits into
developfrom
feature-web-30
Open

feat: 모집공고 조회#23
kimbosung521 wants to merge 2 commits into
developfrom
feature-web-30

Conversation

@kimbosung521

Copy link
Copy Markdown

모집 공고 조회 및 부원 모집 에러 처리

📌 작업 내용

🔗 관련 이슈

📸 스크린샷 / 화면 녹화

🧪 테스트 방법

✅ 체크리스트

  • npm run lint 통과를 확인했습니다
  • npm run format 을 적용했습니다
  • 디버깅용 console.log 를 제거했습니다
  • 사용하지 않는 import / 변수를 정리했습니다
  • 컨벤션 (README.md) 을 준수했습니다
  • base 브랜치가 develop 인지 확인했습니다

💬 리뷰어에게 한 마디

모집 공고 조회 및 부원 모집 에러 처리

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +34
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)}`;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

formatKoreanDatecreateDateRangeText 함수에서 인자로 전달받는 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);
};

Comment on lines +41 to +56
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();
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
    };
  }, []);

Comment on lines +87 to +93
if (response.data?.success === false) {
const errorMessage = response.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE;
setIsConfirmOpen(false);
setSubmitError(errorMessage);
alert(errorMessage);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

커스텀 훅 내부에서 브라우저의 동기식 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;
      }

Comment thread src/hooks/useRecruitForm.js Outdated
Comment on lines +97 to +103
} catch (error) {
if (!isMountedRef.current) return;

setIsConfirmOpen(false);
setSubmitError(
error.response?.data?.message || '모집 신청 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.'
);
const errorMessage = getRecruitSubmitErrorMessage(error);
setSubmitError(errorMessage);
alert(errorMessage);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

마찬가지로 catch 블록 내에서도 브라우저의 동기식 alert를 직접 호출하기보다, submitError 상태 업데이트만 수행하고 UI 단에서 에러를 표현하도록 책임을 분리하는 것이 좋습니다.

Suggested change
} 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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant