Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e9a9f93
feat: 모집 예정 시간 정보 추가
suhyun113 Dec 28, 2025
777ef7e
feat: 지원하기 텍스트 조건 추가
suhyun113 Dec 28, 2025
a8e09ba
feat: 모집 시작 조건 분리
suhyun113 Dec 28, 2025
2e15aea
style: 비활성 상태 스타일 추가
suhyun113 Dec 28, 2025
19dd41a
feat: 모집마감 상태 비활성 추가
suhyun113 Dec 28, 2025
a8c637d
feat: 모집 시작 날짜 텍스트 포맷 조건 수정
suhyun113 Dec 28, 2025
5cd7348
fix: 모집 시작 날짜 포맷 조건 수정
suhyun113 Dec 28, 2025
38b063a
style: 지원하기 버튼 비활성화 색상 수정
suhyun113 Dec 28, 2025
7e37dcc
feat: 모바일 상태 카카오톡 공유 버튼 이미지 추가
suhyun113 Dec 28, 2025
28fcf4c
fix: 모바일 크기 버튼 색상 변경
suhyun113 Dec 28, 2025
f14492a
fix: 전역 폰트 스타일 적용 안 되는 오류 수정
suhyun113 Dec 28, 2025
da843a3
fix: 지원하기 버튼 크기 수정 및 아이콘 변경
suhyun113 Dec 28, 2025
3b87cde
style: 버튼 패딩 및 크기 수정
suhyun113 Dec 28, 2025
46679ea
fix: 스크롤 버튼이 상세페이지 푸터에 가려지는 문제 수정
suhyun113 Dec 28, 2025
793eb83
fix: 모집 상태 상수 분리
suhyun113 Dec 30, 2025
26c680d
fix: 테스트코드 형식 수정
suhyun113 Dec 30, 2025
3f1ef4c
fix: recruitmentStatus로 판단 기준 변경
suhyun113 Dec 30, 2025
4e4d7bc
fix: recruitmentStatus 상태 불러와지지 않는 오류 수정
suhyun113 Dec 30, 2025
5aa9a61
fix: RecruitmentStatus 타입 정의
suhyun113 Dec 30, 2025
ad8c113
fix: RecruitmentStatus 상수 분리
suhyun113 Dec 30, 2025
0214657
fix: 테스트 오류 수정
suhyun113 Dec 30, 2025
816adf4
fix: 테스트 구조 수정
suhyun113 Dec 30, 2025
295a91c
test: 테스트 추가
suhyun113 Dec 30, 2025
841ebb5
refactor: 함수명 변경
suhyun113 Dec 30, 2025
99461a2
fix: 주석 제거 및 네이밍 변경
suhyun113 Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/assets/images/icons/share_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/src/assets/images/icons/share_icon_mobile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const ClubDetailPage = () => {
<ClubDetailFooter
recruitmentStart={clubDetail.recruitmentStart}
recruitmentEnd={clubDetail.recruitmentEnd}
recruitmentStatus={clubDetail.recruitmentStatus}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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%;
Expand All @@ -15,26 +17,30 @@ 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 {
font-size: 12px;
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]};
}
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationForm[]>([]);
const [isApplicationModalOpen, setIsApplicationModalOpen] = useState(false);
const [applicationOptions, setApplicationOptions] = useState<ApplicationForm[]>([]);

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) {
Expand All @@ -44,7 +37,7 @@ const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
}
}
navigate(`/application/${clubId}/${formId}`, { state: { formDetail } });
setIsOpen(false);
setIsApplicationModalOpen(false);
} catch (error) {
console.error('지원서 조회 중 오류가 발생했습니다', error);
alert(
Expand All @@ -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 && (
<>
<Styled.Separator />
{deadlineText}
Expand All @@ -108,14 +105,16 @@ const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => {
return (
<Styled.ApplyButtonContainer>
<ShareButton clubId={clubId} />
<Styled.ApplyButton onClick={handleClick}>
<Styled.ApplyButton
disabled={isRecruitmentUpcoming || isRecruitmentClosed}
onClick={handleApplyButtonClick}>
{renderButtonContent()}
</Styled.ApplyButton>
<ApplicationSelectModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
options={options}
onSelect={openByOption}
isOpen={isApplicationModalOpen}
onClose={() => setIsApplicationModalOpen(false)}
options={applicationOptions}
onSelect={handleSelectApplicationOption}
/>
</Styled.ApplyButtonContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,4 +15,8 @@ export const ClubDetailFooterContainer = styled.div`

background-color: white;
border-top: 1px solid #cdcdcd;

${media.mobile} {
padding: 10px 0px 16px 0px;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { media } from '@/styles/mediaQuery';
import styled from 'styled-components';

export const ShareButtonContainer = styled.div`
Expand All @@ -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;
}
`;
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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();

Expand Down Expand Up @@ -54,7 +57,10 @@ const ShareButton = ({ clubId }: ShareButtonProps) => {
role='button'
aria-label='카카오톡으로 동아리 정보 공유하기'
>
<Styled.ShareButtonIcon src={ShareIcon} alt='카카오톡 공유' />
<Styled.ShareButtonIcon
src={isMobile ? ShareIconMobile : ShareIcon}
alt='카카오톡 공유'
/>
</Styled.ShareButtonContainer>
);
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/styles/Global.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const GlobalStyles = createGlobalStyle`
padding: 0;
box-sizing: border-box;
}
textarea {
textarea, button, input, select {
font-family: 'Pretendard', sans-serif;
}
body {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/types/club.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading