diff --git a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx index 0da782fa..190ea61a 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx @@ -1,18 +1,10 @@ -import { useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; -import { Award, FAQ, IdealCandidate, SemesterTerm } from '@/types/club'; +import { Award, FAQ, IdealCandidate } from '@/types/club'; +import { formatSemesterLabel, getAwardKey } from '@/utils/awardHelpers'; import * as Styled from './ClubIntroContent.styles'; -const formatSemesterLabel = (award: Award): string => { - const semesterLabel = - award.semester === SemesterTerm.FIRST ? '1학기' : '2학기'; - return `${award.year} ${semesterLabel}`; -}; - -const getAwardKey = (award: Award, index: number): string => - `${award.year}-${award.semester}-${index}`; - interface ClubIntroContentProps { activityDescription?: string; awards?: Award[]; @@ -30,21 +22,33 @@ const ClubIntroContent = ({ }: ClubIntroContentProps) => { const trackEvent = useMixpanelTrack(); - const [openFaqIndexes, setOpenFaqIndexes] = useState([]); + const [openFaqIndexes, setOpenFaqIndexes] = useState>(new Set()); - const handleToggleFaq = (index: number) => { - const isOpening = !openFaqIndexes.includes(index); - setOpenFaqIndexes((prev) => - prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index], - ); + const validAwards = useMemo( + () => awards?.filter((award) => formatSemesterLabel(award) !== null) || [], + [awards], + ); - if (faqs && faqs[index]) { - trackEvent(USER_EVENT.FAQ_TOGGLE_CLICKED, { - question: faqs[index].question, - action: isOpening ? 'open' : 'close', + const handleToggleFaq = useCallback( + (index: number) => { + const isOpening = !openFaqIndexes.has(index); + + setOpenFaqIndexes((prev) => { + const newSet = new Set(prev); + if (isOpening) newSet.add(index); + else newSet.delete(index); + return newSet; }); - } - }; + + if (faqs?.[index]) { + trackEvent(USER_EVENT.FAQ_TOGGLE_CLICKED, { + question: faqs[index].question, + action: isOpening ? 'open' : 'close', + }); + } + }, + [faqs, trackEvent, openFaqIndexes], + ); return ( @@ -57,17 +61,16 @@ const ClubIntroContent = ({ )} - {awards && awards.length > 0 && ( + {validAwards.length > 0 && ( 동아리 성과 - {awards.map((award, index) => { + {validAwards.map((award, index) => { + const semesterLabel = formatSemesterLabel(award)!; const awardKey = getAwardKey(award, index); return ( - - {formatSemesterLabel(award)} - + {semesterLabel} {award.achievements.map((item, idx) => ( @@ -102,7 +105,7 @@ const ClubIntroContent = ({ FAQ {faqs.map((faq, index) => { - const isOpen = openFaqIndexes.includes(index); + const isOpen = openFaqIndexes.has(index); return ( handleToggleFaq(index)}> diff --git a/frontend/src/utils/awardHelpers.test.ts b/frontend/src/utils/awardHelpers.test.ts new file mode 100644 index 00000000..a62aa2f2 --- /dev/null +++ b/frontend/src/utils/awardHelpers.test.ts @@ -0,0 +1,99 @@ +import { Award, SemesterTerm, SemesterTermType } from '@/types/club'; +import { formatSemesterLabel, getAwardKey } from './awardHelpers'; + +describe('awardHelpers', () => { + const createAward = ( + year: number, + semester: SemesterTermType, + achievements: string[] = [], + ): Award => ({ + year, + semester, + achievements, + }); + + const validAward2024First = createAward(2024, SemesterTerm.FIRST); + const validAward2024Second = createAward(2024, SemesterTerm.SECOND); + const validAward2023First = createAward(2023, SemesterTerm.FIRST); + const validAward2025Second = createAward(2025, SemesterTerm.SECOND); + + describe('formatSemesterLabel', () => { + it('1학기를 올바른 형식으로 반환해야 한다', () => { + expect(formatSemesterLabel(validAward2024First)).toBe('2024 1학기'); + }); + + it('2학기를 올바른 형식으로 반환해야 한다', () => { + expect(formatSemesterLabel(validAward2024Second)).toBe('2024 2학기'); + }); + + it('year가 없으면 null을 반환해야 한다', () => { + const award: Partial = { + semester: SemesterTerm.FIRST, + achievements: [], + }; + + expect(formatSemesterLabel(award as Award)).toBeNull(); + }); + + it('semester가 없으면 null을 반환해야 한다', () => { + const award: Partial = { + year: 2024, + achievements: [], + }; + + expect(formatSemesterLabel(award as Award)).toBeNull(); + }); + + it('year와 semester가 모두 없으면 null을 반환해야 한다', () => { + const award: Partial = { + achievements: [], + }; + + expect(formatSemesterLabel(award as Award)).toBeNull(); + }); + + it('award가 null이면 null을 반환해야 한다', () => { + expect(formatSemesterLabel(null as unknown as Award)).toBeNull(); + }); + + it('award가 undefined이면 null을 반환해야 한다', () => { + expect(formatSemesterLabel(undefined as unknown as Award)).toBeNull(); + }); + + it('다양한 연도를 올바르게 처리해야 한다', () => { + expect(formatSemesterLabel(validAward2023First)).toBe('2023 1학기'); + expect(formatSemesterLabel(validAward2025Second)).toBe('2025 2학기'); + }); + }); + + describe('getAwardKey', () => { + it('year, semester, index를 조합한 고유 키를 생성해야 한다', () => { + expect(getAwardKey(validAward2024First, 0)).toBe('2024-FIRST-0'); + }); + + it('인덱스가 다르면 다른 키를 생성해야 한다', () => { + const key1 = getAwardKey(validAward2024First, 0); + const key2 = getAwardKey(validAward2024First, 1); + + expect(key1).not.toBe(key2); + expect(key1).toBe('2024-FIRST-0'); + expect(key2).toBe('2024-FIRST-1'); + }); + + it('학기가 다르면 다른 키를 생성해야 한다', () => { + expect(getAwardKey(validAward2024First, 0)).toBe('2024-FIRST-0'); + expect(getAwardKey(validAward2024Second, 0)).toBe('2024-SECOND-0'); + }); + + it('연도가 다르면 다른 키를 생성해야 한다', () => { + const award2024 = createAward(2024, SemesterTerm.FIRST); + + expect(getAwardKey(validAward2023First, 0)).toBe('2023-FIRST-0'); + expect(getAwardKey(award2024, 0)).toBe('2024-FIRST-0'); + }); + + it('큰 인덱스 숫자를 올바르게 처리해야 한다', () => { + expect(getAwardKey(validAward2024First, 999)).toBe('2024-FIRST-999'); + }); + }); +}); diff --git a/frontend/src/utils/awardHelpers.ts b/frontend/src/utils/awardHelpers.ts new file mode 100644 index 00000000..849f64ef --- /dev/null +++ b/frontend/src/utils/awardHelpers.ts @@ -0,0 +1,13 @@ +import { Award, SemesterTerm } from '@/types/club'; + +export const formatSemesterLabel = (award: Award): string | null => { + if (award?.year && award?.semester) { + const semesterLabel = + award.semester === SemesterTerm.FIRST ? '1학기' : '2학기'; + return `${award.year} ${semesterLabel}`; + } + return null; +}; + +export const getAwardKey = (award: Award, index: number): string => + `${award.year}-${award.semester}-${index}`;