diff --git a/frontend/src/assets/images/icons/share_icon.svg b/frontend/src/assets/images/icons/share_icon.svg
index 2a41b66ca..a1e5a0f56 100644
--- a/frontend/src/assets/images/icons/share_icon.svg
+++ b/frontend/src/assets/images/icons/share_icon.svg
@@ -1,4 +1,4 @@
diff --git a/frontend/src/assets/images/icons/share_icon_mobile.svg b/frontend/src/assets/images/icons/share_icon_mobile.svg
new file mode 100644
index 000000000..8dfa0ff3f
--- /dev/null
+++ b/frontend/src/assets/images/icons/share_icon_mobile.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
index 192dc08f1..6337039c9 100644
--- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
+++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
@@ -83,6 +83,7 @@ const ClubDetailPage = () => {
>
);
diff --git a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
index fa2ca81f3..9c66b8d9b 100644
--- a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
+++ b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts
@@ -1,4 +1,6 @@
import styled from 'styled-components';
+import { media } from '@/styles/mediaQuery';
+import { colors } from '@/styles/theme/colors';
export const ApplyButtonContainer = styled.div`
width: 100%;
@@ -15,17 +17,17 @@ export const ApplyButton = styled.button`
justify-content: center;
border: none;
border-radius: 10px;
- cursor: pointer;
+ cursor: ${({disabled}) => (disabled ? 'default' : 'pointer')};
transition: transform 0.2s ease-in-out;
- background-color: #ff7543;
+ background-color: ${({ disabled }) => disabled ? colors.gray[500] : colors.primary[800]};
padding: 10px 40px;
width: 517px;
- height: 50px;
+ height: 60px;
font-size: 20px;
font-style: normal;
font-weight: 700;
- color: #fff;
+ color: ${colors.gray[200]};
text-align: center;
img {
@@ -33,8 +35,12 @@ export const ApplyButton = styled.button`
font-weight: 600;
}
- @media (max-width: 500px) {
- width: 280px;
+ ${media.mobile} {
+ width: 273px;
+ height: 44px;
+ font-size: 16px;
+ font-weight: 500;
+ background-color: ${({ disabled }) => disabled ? colors.gray[500] : colors.gray[900]};
}
`;
diff --git a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
index b7b72c630..517ce496a 100644
--- a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
@@ -14,25 +14,18 @@ interface ClubApplyButtonProps {
deadlineText?: string;
}
-const RECRUITMENT_STATUS = {
- ALWAYS: '상시 모집',
- CLOSED: '모집 마감',
-};
-
const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
const { clubId } = useParams<{ clubId: string }>();
const navigate = useNavigate();
const trackEvent = useMixpanelTrack();
const { data: clubDetail } = useGetClubDetail(clubId!);
- // 모달 옵션 상태
- const [isOpen, setIsOpen] = useState(false);
- const [options, setOptions] = useState([]);
+ const [isApplicationModalOpen, setIsApplicationModalOpen] = useState(false);
+ const [applicationOptions, setApplicationOptions] = useState([]);
if (!clubId || !clubDetail) return null;
- // 내부 폼 이동
- const goWithForm = async (formId: string) => {
+ const navigateToApplicationForm = async (formId: string) => {
try {
const formDetail = await getApplication(clubId, formId);
if (formDetail?.formMode === ApplicationFormMode.EXTERNAL) {
@@ -44,7 +37,7 @@ const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
}
}
navigate(`/application/${clubId}/${formId}`, { state: { formDetail } });
- setIsOpen(false);
+ setIsApplicationModalOpen(false);
} catch (error) {
console.error('지원서 조회 중 오류가 발생했습니다', error);
alert(
@@ -53,49 +46,53 @@ const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
}
};
- // url 존재 시 외부, 내부 지원서 옵션에 따른 처리
- const openByOption = (option?: ApplicationForm) => {
+ const handleSelectApplicationOption = (option?: ApplicationForm) => {
if (!option) return;
- void goWithForm(option.id);
+ void navigateToApplicationForm(option.id);
};
- const handleClick = async () => {
+ const handleApplyButtonClick = async () => {
trackEvent(USER_EVENT.CLUB_APPLY_BUTTON_CLICKED);
- if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
+ if (isRecruitmentClosed) {
alert(`현재 ${clubDetail.name} 동아리는 모집 기간이 아닙니다.`);
return;
}
try {
- const list = await getApplicationOptions(clubId);
+ const forms = await getApplicationOptions(clubId);
- if (list.length <= 0) {
+ if (forms.length <= 0) {
return;
}
- if (list.length === 1) {
- await goWithForm(list[0].id);
+ if (forms.length === 1) {
+ await navigateToApplicationForm(forms[0].id);
return;
}
- setOptions(list);
- setIsOpen(true);
+ setApplicationOptions(forms);
+ setIsApplicationModalOpen(true);
} catch (e) {
- setOptions([]);
- setIsOpen(true);
+ setApplicationOptions([]);
+ setIsApplicationModalOpen(true);
console.error('지원서 옵션 조회 중 오류가 발생했습니다.', e);
}
};
+ const recruitmentStatus = clubDetail.recruitmentStatus;
+ const isRecruitmentClosed = recruitmentStatus === 'CLOSED';
+ const isRecruitmentUpcoming = recruitmentStatus === 'UPCOMING';
+ const isAlwaysRecruiting = recruitmentStatus === 'ALWAYS';
+
const renderButtonContent = () => {
- if (deadlineText === RECRUITMENT_STATUS.CLOSED) {
- return RECRUITMENT_STATUS.CLOSED;
+ if (isRecruitmentClosed || isRecruitmentUpcoming) {
+ return deadlineText;
}
return (
<>
지원하기
- {deadlineText && deadlineText !== RECRUITMENT_STATUS.ALWAYS && (
+ {!isAlwaysRecruiting && deadlineText && (
<>
{deadlineText}
@@ -108,14 +105,16 @@ const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
return (
-
+
{renderButtonContent()}
setIsOpen(false)}
- options={options}
- onSelect={openByOption}
+ isOpen={isApplicationModalOpen}
+ onClose={() => setIsApplicationModalOpen(false)}
+ options={applicationOptions}
+ onSelect={handleSelectApplicationOption}
/>
);
diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
index a27fe5716..559644739 100644
--- a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
+++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts
@@ -1,11 +1,12 @@
+import { media } from '@/styles/mediaQuery';
import styled from 'styled-components';
export const ClubDetailFooterContainer = styled.div`
position: sticky;
bottom: 0;
width: 100%;
- z-index: 1050; // TODO: Portal로 모달 분리 후 header보다 낮게 재조정
- padding: 10px 40px;
+ z-index: 1040; // TODO: Portal로 모달 분리 후 header보다 낮게 재조정
+ padding: 10px 0px 24px 0px;
display: flex;
align-items: center;
@@ -14,4 +15,8 @@ export const ClubDetailFooterContainer = styled.div`
background-color: white;
border-top: 1px solid #cdcdcd;
+
+ ${media.mobile} {
+ padding: 10px 0px 16px 0px;
+ }
`;
\ No newline at end of file
diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
index 442007ff3..52b4c3184 100644
--- a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx
@@ -2,20 +2,23 @@ import getDeadlineText from '@/utils/getDeadLineText';
import { recruitmentDateParser } from '@/utils/recruitmentDateParser';
import ClubApplyButton from '../ClubApplyButton/ClubApplyButton';
import * as Styled from './ClubDetailFooter.styles';
+import { RecruitmentStatus } from '@/types/club';
interface ClubDetailFooterProps {
recruitmentStart: string;
recruitmentEnd: string;
+ recruitmentStatus: RecruitmentStatus;
}
const ClubDetailFooter = ({
recruitmentStart,
recruitmentEnd,
+ recruitmentStatus,
}: ClubDetailFooterProps) => {
const deadlineText = getDeadlineText(
recruitmentDateParser(recruitmentStart),
recruitmentDateParser(recruitmentEnd),
- new Date(),
+ recruitmentStatus,
);
return (
diff --git a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
index ea18b7e0d..7f4c66d22 100644
--- a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
+++ b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts
@@ -1,3 +1,4 @@
+import { media } from '@/styles/mediaQuery';
import styled from 'styled-components';
export const ShareButtonContainer = styled.div`
@@ -7,6 +8,11 @@ export const ShareButtonContainer = styled.div`
`;
export const ShareButtonIcon = styled.img`
- width: 50px;
- height: 50px;
+ width: 60px;
+ height: 60px;
+
+ ${media.mobile} {
+ width: 44px;
+ height: 44px;
+ }
`;
\ No newline at end of file
diff --git a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
index ff6a7ec70..910561794 100644
--- a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
@@ -1,7 +1,9 @@
import ShareIcon from '@/assets/images/icons/share_icon.svg';
+import ShareIconMobile from '@/assets/images/icons/share_icon_mobile.svg';
import { USER_EVENT } from '@/constants/eventName';
import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail';
import useMixpanelTrack from '@/hooks/useMixpanelTrack';
+import useDevice from '@/hooks/useDevice';
import * as Styled from './ShareButton.styles';
interface ShareButtonProps {
@@ -13,6 +15,7 @@ const DEFAULT_IMAGE_URL =
'https://avatars.githubusercontent.com/u/200371900?s=200&v=4';
const ShareButton = ({ clubId }: ShareButtonProps) => {
+ const { isMobile } = useDevice();
const { data: clubDetail } = useGetClubDetail(clubId);
const trackEvent = useMixpanelTrack();
@@ -54,7 +57,10 @@ const ShareButton = ({ clubId }: ShareButtonProps) => {
role='button'
aria-label='카카오톡으로 동아리 정보 공유하기'
>
-
+
);
};
diff --git a/frontend/src/styles/Global.styles.ts b/frontend/src/styles/Global.styles.ts
index 8a4bf3b66..7427302a4 100644
--- a/frontend/src/styles/Global.styles.ts
+++ b/frontend/src/styles/Global.styles.ts
@@ -6,7 +6,7 @@ const GlobalStyles = createGlobalStyle`
padding: 0;
box-sizing: border-box;
}
- textarea {
+ textarea, button, input, select {
font-family: 'Pretendard', sans-serif;
}
body {
diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts
index a77ed5dba..d38e5c3be 100644
--- a/frontend/src/types/club.ts
+++ b/frontend/src/types/club.ts
@@ -1,12 +1,18 @@
import { SNS_CONFIG } from '@/constants/snsConfig';
+export type RecruitmentStatus =
+ | 'OPEN'
+ | 'CLOSED'
+ | 'UPCOMING'
+ | 'ALWAYS';
+
export interface Club {
id: string;
name: string;
logo: string;
cover?: string;
tags: string[];
- recruitmentStatus: string;
+ recruitmentStatus: RecruitmentStatus;
division: string;
category: string;
introduction: string;
diff --git a/frontend/src/utils/getDeadLineText.test.ts b/frontend/src/utils/getDeadLineText.test.ts
index a30998e94..14619aa2c 100644
--- a/frontend/src/utils/getDeadLineText.test.ts
+++ b/frontend/src/utils/getDeadLineText.test.ts
@@ -1,22 +1,82 @@
import getDeadlineText from './getDeadLineText';
-const recruitmentStart = new Date('2025-04-01');
-const recruitmentEnd = new Date('2025-04-10');
-
describe('getDeadlineText 함수 테스트', () => {
it.each([
- ['오늘이 모집 종료일인 경우', '2025-04-10', 'D-Day'],
- ['모집 종료일까지 5일 남은 경우', '2025-04-05', 'D-5'],
- ['오늘이 모집 종료일 이후인 경우', '2025-04-11', '모집 마감'],
- ['모집 시작일이 아직 남은 경우', '2025-03-30', '모집 전'],
- ])('%s', (_, todayStr, expected) => {
- const today = new Date(todayStr);
- expect(getDeadlineText(recruitmentStart, recruitmentEnd, today)).toBe(
+ [
+ '오늘이 모집 종료일인 경우',
+ new Date('2025-04-01'),
+ new Date('2025-04-10'),
+ '2025-04-10',
+ 'OPEN',
+ 'D-Day'
+ ],
+ [
+ '모집 종료일까지 5일 남은 경우',
+ new Date('2025-04-01'),
+ new Date('2025-04-10'),
+ '2025-04-05',
+ 'OPEN',
+ 'D-5'
+ ],
+ [
+ '오늘이 모집 종료일 이후인 경우',
+ new Date('2025-04-01'),
+ new Date('2025-04-10'),
+ '2025-04-11',
+ 'CLOSED',
+ '모집 마감'
+ ],
+ [
+ '모집 시작일이 아직 남은 경우 (시간 포함)',
+ new Date('2025-04-01T09:00:00'),
+ new Date('2025-04-10'),
+ '2025-03-30',
+ 'UPCOMING',
+ '4월 1일 09:00 모집 시작'
+ ],
+ [
+ '모집 시작 시간이 00:00인 경우',
+ new Date('2025-04-01T00:00:00'),
+ new Date('2025-04-10'),
+ '2025-03-30',
+ 'UPCOMING',
+ '4월 1일 모집 시작'
+ ],
+
+ ])('%s',
+ (
+ _,
+ recruitmentStart,
+ recruitmentEnd,
+ todayStr,
+ recruitmentStatus,
expected,
- );
+ ) => {
+ const today = new Date(todayStr);
+ expect(
+ getDeadlineText(
+ recruitmentStart,
+ recruitmentEnd,
+ recruitmentStatus,
+ today
+ )
+ ).toBe(
+ expected);
});
it('모집 기간이 null인 경우 모집 마감을 반환해야 한다', () => {
- expect(getDeadlineText(null, null)).toBe('모집 마감');
+ expect(
+ getDeadlineText(null, null, 'CLOSED')).toBe('모집 마감');
});
+
+ it('모집 중 상태인데 모집 종료일까지 1년 이상 남으면 상시 모집을 반환해야 한다', () => {
+ expect(
+ getDeadlineText(
+ new Date('2025-01-01'),
+ new Date('2027-01-01'),
+ 'OPEN',
+ new Date('2025-01-01'),
+ ),
+ ).toBe('상시 모집');
+});
});
diff --git a/frontend/src/utils/getDeadLineText.ts b/frontend/src/utils/getDeadLineText.ts
index 9ba813293..d6a04a4ea 100644
--- a/frontend/src/utils/getDeadLineText.ts
+++ b/frontend/src/utils/getDeadLineText.ts
@@ -1,17 +1,40 @@
-import { differenceInCalendarDays, isAfter, isBefore } from 'date-fns';
+import { format, differenceInCalendarDays } from 'date-fns';
+import { ko } from 'date-fns/locale';
+
+const RECRUITMENT_STATUS = {
+ CLOSED: '모집 마감',
+ ALWAYS: '상시 모집',
+ UPCOMING: '모집 시작',
+};
const getDeadlineText = (
recruitmentStart: Date | null,
recruitmentEnd: Date | null,
+ recruitmentStatus: string,
today: Date = new Date(),
): string => {
- if (!recruitmentStart || !recruitmentEnd) return '모집 마감';
- if (isBefore(today, recruitmentStart)) return '모집 전';
- if (isAfter(today, recruitmentEnd)) return '모집 마감';
+ console.log(recruitmentStart, recruitmentEnd, recruitmentStatus, today);
+
+ if (recruitmentStatus === 'CLOSED') {
+ return RECRUITMENT_STATUS.CLOSED;
+ }
+
+ if (recruitmentStatus === 'UPCOMING') {
+ if (!recruitmentStart) return RECRUITMENT_STATUS.CLOSED;
+ const hour = recruitmentStart.getHours();
+ const minute = recruitmentStart.getMinutes();
+
+ let formatStr =
+ hour === 0 && minute === 0
+ ? 'M월 d일'
+ : 'M월 d일 HH:mm';
+ return `${format(recruitmentStart, formatStr, { locale: ko })} ${RECRUITMENT_STATUS.UPCOMING}`;
+ }
+ if (!recruitmentEnd) return RECRUITMENT_STATUS.CLOSED;
const days = differenceInCalendarDays(recruitmentEnd, today);
- if (days > 365) return '상시 모집';
+ if (days > 365) return RECRUITMENT_STATUS.ALWAYS; // D-day가 의미 없을 정도로 긴 경우 '상시 모집'으로 표시
return days > 0 ? `D-${days}` : 'D-Day';
};