Skip to content
Open
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,9 +1,11 @@
import styled from '@emotion/styled';
import { useNavigate } from '@tanstack/react-router';
import CertificateModal from './CertificateModal';
import { MEDAL_COLORS } from '../../constants/challenge';
import Button from '@/components/Button/Button';
import useModal from '@/components/Modal/useModal';
import type { MyCompletedChallenge } from '@/apis/members/members.api';
import type { KeyboardEvent, MouseEvent } from 'react';
import MedalBronzeIcon from '#/assets/svg/medal-bronze.svg';
import MedalGoldIcon from '#/assets/svg/medal-gold.svg';
import MedalSilverIcon from '#/assets/svg/medal-silver.svg';
Expand All @@ -28,14 +30,43 @@ const CompletedChallengeCard = ({ challenge }: CompletedChallengeCardProps) => {
const { challengeId, title, startDate, endDate, attendanceRate, grade } =
challenge;
const { modalRef, isOpen, openModal, closeModal } = useModal();
const navigate = useNavigate();

const medalGrade = grade != null && grade !== 'FAIL' ? grade : null;
const isFail = grade === 'FAIL';
const medalGrade = grade != null && !isFail ? grade : null;
const MedalIcon = medalGrade ? MEDAL_ICON[medalGrade] : null;
const gradeColor = medalGrade ? GRADE_COLOR[medalGrade] : null;

const handleCardClick = () => {
if (isFail || !challengeId) return;

navigate({
to: '/challenge/$challengeId/dashboard',
params: { challengeId: String(challengeId) },
});
};
Comment on lines +40 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Major] challengeId가 없는 완료 챌린지에서 /challenge/undefined/dashboard로 이동할 수 있음

MyCompletedChallenge.challengeId는 OpenAPI 스키마상 number | undefined입니다(web/src/types/openapi.d.tsCompletedChallengeResponse.challengeId?: number). 실제로 90번째 줄에서는 CertificateModal을 렌더링할 때 challengeId !== undefined를 명시적으로 체크하고 있어, 이 값이 비어 있을 수 있다는 걸 이미 인지하고 있는 것으로 보입니다.

하지만 handleCardClickisFail만 체크하고 challengeId의 존재 여부는 확인하지 않습니다. 등급이 FAIL이 아니면서 challengeId가 없는 카드를 클릭하면 String(challengeId)"undefined" 문자열이 되어 /challenge/undefined/dashboard로 이동하게 됩니다.

Claude 검증: 변경 diff의 web/src/pages/my-page/components/MyChallengeSection/CompletedChallengeCard.tsx:40-47 기준으로 확인했습니다. 실제 코드와 다르면 이 코멘트는 무시해 주세요. (confidence 0.75).

수정 제안:
이 finding이 여전히 유효한지 먼저 확인한 뒤 (예: 실제 API 응답에서 완료 챌린지의 challengeId가 항상 존재하는지), 유효하다면 handleCardClick에서 if (isFail || challengeId === undefined) return;처럼 가드를 추가하는 것을 검토해 주세요.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

챌린지 id가 없으면 클릭 이벤트가 발생하지 않도록 가드를 추가했습니다

fix: 챌린지 id가 없을때 페이지로 라우팅 되지 않도록 개선


const handleCertButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
openModal();
};

const handleCardKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleCardClick();
}
};

return (
<>
<Container>
<Container
disabled={isFail}
onClick={handleCardClick}
onKeyDown={handleCardKeyDown}
role="button"
tabIndex={isFail ? -1 : 0}
>
<MedalCircle gradeColor={gradeColor}>
{MedalIcon ? (
<MedalIcon width={36} height={36} />
Expand All @@ -56,7 +87,11 @@ const CompletedChallengeCard = ({ challenge }: CompletedChallengeCardProps) => {
{grade === 'FAIL' && <FailBadge>(탈락)</FailBadge>}
</AttendanceText>
{grade !== 'FAIL' && (
<CertButton variant="transparent" onClick={openModal}>
<CertButton
variant="transparent"
onClick={handleCertButtonClick}
disabled={isFail}
>
수료증 확인
</CertButton>
)}
Expand All @@ -79,7 +114,7 @@ const CompletedChallengeCard = ({ challenge }: CompletedChallengeCardProps) => {

export default CompletedChallengeCard;

const Container = styled.div`
const Container = styled.div<{ disabled?: boolean }>`
width: 100%;
padding: 12px 16px;
border: 1px solid ${({ theme }) => theme.colors.stroke};
Expand All @@ -89,12 +124,22 @@ const Container = styled.div`
gap: 12px;
align-items: center;

background-color: ${({ theme, disabled }) =>
disabled ? theme.colors.disabledBackground : theme.colors.white};

box-sizing: border-box;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
`;

const Content = styled.div`
width: 100%;
min-width: 0;

display: flex;
gap: 4px;
flex: 1;
flex-direction: column;
align-items: flex-start;
`;

const MedalCircle = styled.div<{ gradeColor: string | null }>`
Expand All @@ -108,7 +153,7 @@ const MedalCircle = styled.div<{ gradeColor: string | null }>`
justify-content: center;

background-color: ${({ theme, gradeColor }) =>
gradeColor ? `${gradeColor}20` : theme.colors.dividers};
gradeColor ? `${gradeColor}20` : theme.colors.stroke};
`;

const MedalPlaceholder = styled.div`
Expand All @@ -126,11 +171,13 @@ const MedalPlaceholder = styled.div`
`;

const Info = styled.div`
width: 100%;
min-width: 0;

display: flex;
gap: 4px;
flex-direction: column;
align-items: flex-start;
`;

const Title = styled.h3`
Expand Down Expand Up @@ -167,6 +214,8 @@ const FailBadge = styled.span`
`;

const BottomRow = styled.div`
width: 100%;

display: flex;
align-items: center;
justify-content: space-between;
Expand Down
Loading