Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -30,21 +22,33 @@ const ClubIntroContent = ({
}: ClubIntroContentProps) => {
const trackEvent = useMixpanelTrack();

const [openFaqIndexes, setOpenFaqIndexes] = useState<number[]>([]);
const [openFaqIndexes, setOpenFaqIndexes] = useState<Set<number>>(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 (
<Styled.Container>
Expand All @@ -57,17 +61,16 @@ const ClubIntroContent = ({
</Styled.Section>
)}

{awards && awards.length > 0 && (
{validAwards.length > 0 && (
<Styled.Section>
<Styled.SectionTitle>동아리 성과</Styled.SectionTitle>
<Styled.TextContainer>
{awards.map((award, index) => {
{validAwards.map((award, index) => {
const semesterLabel = formatSemesterLabel(award)!;
const awardKey = getAwardKey(award, index);
return (
<Styled.AwardGroup key={awardKey}>
<Styled.SemesterBadge>
{formatSemesterLabel(award)}
</Styled.SemesterBadge>
<Styled.SemesterBadge>{semesterLabel}</Styled.SemesterBadge>
<Styled.AwardList>
{award.achievements.map((item, idx) => (
<Styled.AwardItem key={`${awardKey}-${idx}`}>
Expand Down Expand Up @@ -102,7 +105,7 @@ const ClubIntroContent = ({
<Styled.FaqHeader>FAQ</Styled.FaqHeader>
<Styled.FaqList>
{faqs.map((faq, index) => {
const isOpen = openFaqIndexes.includes(index);
const isOpen = openFaqIndexes.has(index);
return (
<Styled.FaqItem key={faq.question}>
<Styled.QuestionRow onClick={() => handleToggleFaq(index)}>
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/utils/awardHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -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<Award> = {
semester: SemesterTerm.FIRST,
achievements: [],
};

expect(formatSemesterLabel(award as Award)).toBeNull();
});

it('semester가 없으면 null을 반환해야 한다', () => {
const award: Partial<Award> = {
year: 2024,
achievements: [],
};

expect(formatSemesterLabel(award as Award)).toBeNull();
});

it('year와 semester가 모두 없으면 null을 반환해야 한다', () => {
const award: Partial<Award> = {
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');
});
});
});
13 changes: 13 additions & 0 deletions frontend/src/utils/awardHelpers.ts
Original file line number Diff line number Diff line change
@@ -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}`;