-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 모집공고 조회 #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: 모집공고 조회 #23
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,81 @@ | ||
| 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 (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)}`; | ||
| }; | ||
|
|
||
| const RecruitCompleteCard = () => { | ||
| const [recruitment, setRecruitment] = useState(null); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
| 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 { | ||
| if (isMounted) { | ||
| setIsLoading(false); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| fetchMainRecruitment(); | ||
|
|
||
| return () => { | ||
| isMounted = false; | ||
| }; | ||
| }, []); | ||
|
Comment on lines
+43
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이는 메모리 누수를 유발할 수 있으므로, 컴포넌트의 마운트 여부를 추적하는 로컬 변수( 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;
};
}, []); |
||
|
|
||
| const interviewPeriod = createDateRangeText( | ||
| recruitment?.interviewStart, | ||
| recruitment?.interviewEnd | ||
| ); | ||
| const notificationDate = formatKoreanDate(recruitment?.notificationDate); | ||
| const contactText = [recruitment?.bossName, recruitment?.contactNumber].filter(Boolean).join(' '); | ||
|
|
||
| return ( | ||
| <div className="mx-auto flex min-h-[620px] w-full max-w-[1253px] items-center justify-center rounded-[28px] bg-white px-5 py-12 shadow-recruit-complete sm:min-h-[700px] sm:rounded-[38px] sm:px-8 lg:min-h-[802px] lg:rounded-[50px]"> | ||
| <div className="w-full max-w-[1040px] text-center text-ink"> | ||
|
|
@@ -12,26 +85,42 @@ function RecruitCompleteCard() { | |
| </h2> | ||
|
|
||
| <div className="mx-auto mt-9 w-full max-w-[650px] space-y-5 text-left text-[clamp(16px,4.8vw,27px)] font-light leading-[1.45] sm:mt-12 sm:space-y-6 lg:mt-[54px] lg:w-fit lg:max-w-full lg:space-y-[29px] lg:leading-[1.35]"> | ||
| <p className="lg:whitespace-nowrap"> | ||
| <span className="font-normal text-brand">면접 기간: </span> | ||
| 2026년 4월 6일 ~ 4월 7일 | ||
| </p> | ||
| <p className="lg:whitespace-nowrap"> | ||
| <span className="font-normal text-brand">합격 통보: </span> | ||
| 2026년 4월 7일 예정{' '} | ||
| <span className="text-[clamp(13px,3.4vw,24px)] text-ink-sub"> | ||
| (해당 일정은 변동될 수 있습니다.) | ||
| </span> | ||
| </p> | ||
| <p className="lg:whitespace-nowrap"> | ||
| <span className="font-normal text-brand">면접 장소: </span> | ||
| 3-511호 ONE 동아리방 | ||
| </p> | ||
|
|
||
| <p className="pt-8 lg:whitespace-nowrap lg:pt-[42px]"> | ||
| 회장의 연락을 통해 면접 일정이 조정될 예정이오니, 잠시 기다려 주세요. | ||
| </p> | ||
| <p className="lg:whitespace-nowrap">문의: 회장 최예은 010-1111-1111</p> | ||
| {isLoading && <p className="text-center">모집 공고 정보를 불러오는 중입니다.</p>} | ||
|
|
||
| {!isLoading && hasRecruitmentError && ( | ||
| <p className="text-center text-error" role="alert"> | ||
| {EMPTY_RECRUITMENT_NOTICE} | ||
| </p> | ||
| )} | ||
|
|
||
| {!isLoading && !hasRecruitmentError && recruitment && ( | ||
| <> | ||
| {interviewPeriod && ( | ||
| <p className="lg:whitespace-nowrap"> | ||
| <span className="font-normal text-brand">면접 기간: </span> | ||
| {interviewPeriod} | ||
| </p> | ||
| )} | ||
| {notificationDate && ( | ||
| <p className="lg:whitespace-nowrap"> | ||
| <span className="font-normal text-brand">합격 통보: </span> | ||
| {notificationDate} 예정{' '} | ||
| <span className="text-[clamp(13px,3.4vw,24px)] text-ink-sub"> | ||
| (해당 일정은 변동될 수 있습니다.) | ||
| </span> | ||
| </p> | ||
| )} | ||
| {recruitment.roomLocation && ( | ||
| <p className="lg:whitespace-nowrap"> | ||
| <span className="font-normal text-brand">면접 장소: </span> | ||
| {recruitment.roomLocation} | ||
| </p> | ||
| )} | ||
| </> | ||
| )} | ||
|
|
||
| <p className="pt-8 lg:whitespace-nowrap lg:pt-[42px]">{COMPLETE_MESSAGE}</p> | ||
| {contactText && <p className="lg:whitespace-nowrap">문의: 회장 {contactText}</p>} | ||
| </div> | ||
|
|
||
| <Link | ||
|
|
@@ -43,6 +132,6 @@ function RecruitCompleteCard() { | |
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| export default RecruitCompleteCard; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,24 @@ 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); | ||
| return; | ||
| } | ||
|
Comment on lines
+87
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 커스텀 훅 내부에서 브라우저의 동기식 이미 이 훅은 if (response.data?.success === false) {
const errorMessage = response.data?.message ?? DEFAULT_SUBMIT_ERROR_MESSAGE;
setIsConfirmOpen(false);
setSubmitError(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); | ||
| } finally { | ||
| if (isMountedRef.current) { | ||
| setIsSubmitting(false); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatKoreanDate와createDateRangeText함수에서 인자로 전달받는dateText,startDate,endDate가 문자열 타입이 아니거나 예상치 못한 형식일 경우,split메서드 호출 시TypeError가 발생하거나 잘못된 날짜 텍스트(NaN년 NaN월 NaN일)가 생성될 수 있습니다.방어적 프로그래밍(Defensive Programming) 관점에서 인자의 타입을 검사하고, 변환된 월/일 값이 유효한 숫자인지(
Number.isNaN) 확인하는 예외 처리를 추가하여 애플리케이션의 안정성을 높이는 것을 권장합니다.