diff --git a/src/apis/challenge-completes/challenge-completes.api.ts b/src/apis/challenge-completes/challenge-completes.api.ts new file mode 100644 index 0000000..0b956bf --- /dev/null +++ b/src/apis/challenge-completes/challenge-completes.api.ts @@ -0,0 +1,27 @@ +import { axiosClient } from '../AxiosClient'; +import { ChallengeCompletesResponse } from './challenge-completes.response'; +import { useQuery } from '@tanstack/react-query'; + +export const challengeCompletesPath = () => '/api/user/challenges/completes'; + +export const ChallengeCompletesQueryKey = [challengeCompletesPath()]; + +export const getChallengeCompletes = async ( + page: number, + size: number +): Promise => { + const response = await axiosClient.get(challengeCompletesPath(), { + params: { + page, + size, + }, + }); + return response.data; +}; + +export const useGetChallengeCompletes = (page: number, size: number) => { + return useQuery({ + queryKey: [ChallengeCompletesQueryKey, page, size], + queryFn: () => getChallengeCompletes(page, size), + }); +}; diff --git a/src/apis/challenge-completes/challenge-completes.response.ts b/src/apis/challenge-completes/challenge-completes.response.ts new file mode 100644 index 0000000..feec96c --- /dev/null +++ b/src/apis/challenge-completes/challenge-completes.response.ts @@ -0,0 +1,18 @@ +import ApiResponse from '../ApiResponse'; + +export type ChallengeData = { + id: number; + challengeGroupId: number; + title: string; + successDate: string; + category: 'HEALTH' | 'ECHO' | 'SHARE' | 'VOLUNTEER'; + reviewWritten: boolean; +}; + +export type ChallengeCompletes = { + totalPage: number; + hasNext: boolean; + data: ChallengeData[]; +}; + +export type ChallengeCompletesResponse = ApiResponse; diff --git a/src/components/common/cta/index.tsx b/src/components/common/cta/index.tsx index a54a961..c3c5ce8 100644 --- a/src/components/common/cta/index.tsx +++ b/src/components/common/cta/index.tsx @@ -1,15 +1,27 @@ import styled from '@emotion/styled'; type CTAProps = { + theme?: 'primary' | 'secondary'; label: string; display?: 'flex' | 'block'; disabled?: boolean; onClick: () => void; }; -const CTA = ({ label, display = 'flex', disabled, onClick }: CTAProps) => { +const CTA = ({ + theme = 'primary', + label, + display = 'flex', + disabled, + onClick, +}: CTAProps) => { return ( - + {label} ); @@ -20,13 +32,17 @@ export default CTA; export const CTA_CONTAINER_HEIGHT = '4rem'; const StyledCTA = styled.button<{ + theme: 'primary' | 'secondary'; display: 'flex' | 'block'; disabled?: boolean; }>` - border: none; + border: ${({ theme }) => + theme === 'primary' ? `none` : `1px solid var(--color-green-01)`}; border-radius: 10px; - background-color: var(--color-green-01); - color: var(--color-white); + background-color: ${({ theme }) => + theme === 'primary' ? `var(--color-green-01)` : `var(--color-white)`}; + color: ${({ theme }) => + theme === 'primary' ? `var(--color-white)` : `var(--color-green-01)`}; outline: none; ${({ display }) => @@ -42,7 +58,6 @@ const StyledCTA = styled.button<{ ${({ display }) => display === 'block' && ` - margin: 0 0 0 auto; padding: 6px 8px; font-size: var(--font-size-sm); font-weight: 600; diff --git a/src/components/common/empty-state/index.tsx b/src/components/common/empty-state/index.tsx index 700f54b..fdecec8 100644 --- a/src/components/common/empty-state/index.tsx +++ b/src/components/common/empty-state/index.tsx @@ -30,5 +30,6 @@ const Wrapper = styled.div` .highlight { font-weight: 600; color: var(--color-green-03); + text-align: center; } `; diff --git a/src/interface/apis/challenge/index.ts b/src/interface/apis/challenge/index.ts index 1b6de75..b0cb667 100644 --- a/src/interface/apis/challenge/index.ts +++ b/src/interface/apis/challenge/index.ts @@ -1,4 +1,5 @@ export interface ChallengeData { + challengeGroupId: number; challengeId: number; title: string; successCount: number; diff --git a/src/pages/challenge-list/components/contents/index.tsx b/src/pages/challenge-list/components/contents/index.tsx index b9e4f11..fa86280 100644 --- a/src/pages/challenge-list/components/contents/index.tsx +++ b/src/pages/challenge-list/components/contents/index.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; +import { formatDate } from '@/utils/formatters'; import { Box, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; @@ -29,7 +30,7 @@ const Contents = ({ onClick(id); }; - const date = `${startDate} ~ ${endDate}`; + const period = `${formatDate(startDate)} ~ ${formatDate(endDate)}`; return ( @@ -50,9 +51,9 @@ const Contents = ({ - 참여 가능 기간 + 챌린지 신청 가능 기간 - {date} + {period} ); diff --git a/src/pages/challenge-record/components/caution/index.tsx b/src/pages/challenge-record/components/caution/index.tsx index faa585d..81f0e9c 100644 --- a/src/pages/challenge-record/components/caution/index.tsx +++ b/src/pages/challenge-record/components/caution/index.tsx @@ -12,15 +12,17 @@ const Caution = () => { listStyleType='disc' listStylePosition='inside' fontSize='var(--font-size-xs)' + css={` + li { + padding-left: 1.4em; /* 들여쓰기 추가 */ + text-indent: -1.4em; /* 첫 줄은 들여쓰지 않음 */ + } + `} > -
  • 모든 스탬프를 모으면 챌린지를 완수하게 됩니다.
  • +
  • 1일 인증 횟수에는 제한이 없습니다.
  • +
  • 모든 스탬프를 모으면 챌린지를 완료하게 됩니다.
  • - 스탬프는 하루 1개로 제한됩니다. (동일 챌린지를 하루에 여러 번 - 인증하더라도 1회만 인정됩니다.) -
  • -
  • - 명시된 횟수를 초과한 경우 챌린지 완수로 인정되나 추가 인증에 대한 - 스탬프나 포인트는 제공되지 않습니다. + 완료한 챌린지는 '완료한 챌린지 목록'에서 조회할 수 있습니다.
  • 사진 조작, 타인의 계정 이용 등의 부정 행위 적발 시 해당 계정은 강제 diff --git a/src/pages/my-challenge-record/components/list-item.tsx b/src/pages/my-challenge-record/components/list-item.tsx index 6da7be0..6873fda 100644 --- a/src/pages/my-challenge-record/components/list-item.tsx +++ b/src/pages/my-challenge-record/components/list-item.tsx @@ -1,24 +1,22 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate, Link } from 'react-router-dom'; import ProfileImg from '@/assets/challenge/ZZAN-Green.png'; import CTA from '@/components/common/cta'; import { RouterPath } from '@/routes/path'; -import { Image } from '@chakra-ui/react'; +import { Box, Image, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; type Props = { challengeId: number; challengeTitle: string; - userNickname: string; - profileImageUrl?: string | null; }; -const ListItem = ({ challengeId, challengeTitle, profileImageUrl }: Props) => { - sessionStorage.setItem('activeTab', '0'); // 선택 탭 초기화 +const ListItem = ({ challengeId, challengeTitle }: Props) => { + sessionStorage.setItem('activeTab', '0'); const navigate = useNavigate(); - const handleChallengeClick = ( + const handleViewRecord = ( challengeId: number, title: string, category?: string @@ -51,22 +49,24 @@ const ListItem = ({ challengeId, challengeTitle, profileImageUrl }: Props) => { return ( - profile + profile - handleChallengeClick(challengeId, challengeTitle)} - > - {challengeTitle} - - handleReviewWrite(challengeId, challengeTitle)} - /> + + {challengeTitle} + + + handleViewRecord(challengeId, challengeTitle)} + /> + handleReviewWrite(challengeId, challengeTitle)} + /> + ); }; @@ -100,12 +100,11 @@ const ProfileContainer = styled.div` padding: 0.5rem; `; -const ChallengeTitle = styled.button` +const ChallengeTitle = styled(Text)` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; font-size: 1rem; + flex: 1; `; - -const ReviewWriteButton = styled(CTA)``; diff --git a/src/pages/my-challenge-record/index.tsx b/src/pages/my-challenge-record/index.tsx index 9051c5d..180c867 100644 --- a/src/pages/my-challenge-record/index.tsx +++ b/src/pages/my-challenge-record/index.tsx @@ -1,9 +1,10 @@ import { useCallback, useEffect, useState } from 'react'; import ListItem from './components/list-item'; -import { useGetReview } from '@/apis/my-challenge-record/getReview.api'; -import { ChallengeData } from '@/apis/my-challenge-record/getReview.response'; +import { useGetChallengeCompletes } from '@/apis/challenge-completes/challenge-completes.api'; +import { ChallengeData } from '@/apis/challenge-completes/challenge-completes.response'; import EmptyState from '@/components/common/empty-state'; +import { NAVBAR_HEIGHT } from '@/components/features/layout/nav-bar'; import TopBar, { HEADER_HEIGHT } from '@/components/features/layout/top-bar'; import { Box, Spinner } from '@chakra-ui/react'; import styled from '@emotion/styled'; @@ -11,7 +12,7 @@ import styled from '@emotion/styled'; const MyChallengeRecord = () => { const [page, setPage] = useState(0); const [allChallenges, setAllChallenges] = useState([]); - const { data, isLoading } = useGetReview(page, 20); + const { data, isLoading } = useGetChallengeCompletes(page, 20); const loadMoreChallenges = useCallback(() => { if (data?.data.hasNext && !isLoading) { @@ -54,22 +55,15 @@ const MyChallengeRecord = () => { {allChallenges.length > 0 ? ( allChallenges.map((challenge, index) => ( )) ) : ( - - - 완료한 챌린지가 존재하지 않습니다. - -
    - 어서 챌린지를 인증하여 스탬프를 채워보세요! -
    +

    완료한 챌린지가 존재하지 않습니다.

    +

    어서 챌린지를 인증하여 스탬프를 채워보세요!

    )} @@ -86,9 +80,8 @@ const MyChallengeRecord = () => { export default MyChallengeRecord; const MyChallengeRecordLayout = styled.div` - min-height: calc( - 100vh - ${HEADER_HEIGHT} - ); // 부모가 block이므로 해당 요소에 직접 높이 지정 + min-height: calc(100vh - ${HEADER_HEIGHT} - ${NAVBAR_HEIGHT}); + // 부모가 block이므로 해당 요소에 직접 높이 지정 display: flex; flex-direction: column; background-color: var(--color-green-06); diff --git a/src/pages/my-challenge/components/challenge-list/index.tsx b/src/pages/my-challenge/components/challenge-list/index.tsx index f6b70c6..ebaeaeb 100644 --- a/src/pages/my-challenge/components/challenge-list/index.tsx +++ b/src/pages/my-challenge/components/challenge-list/index.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate, Link } from 'react-router-dom'; import * as S from './styles'; import NotChallenge from '@/assets/UserImage.svg'; @@ -11,15 +11,15 @@ import { Box, Image, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; type ChallengeListProps = { - challenges: ChallengeData[]; + challengeList: ChallengeData[]; }; -const ChallengeList = ({ challenges }: ChallengeListProps) => { +const ChallengeList = ({ challengeList }: ChallengeListProps) => { const navigate = useNavigate(); return ( <> - {challenges.length === 0 ? ( + {challengeList.length === 0 ? ( { ) : ( - {challenges.map((challenge, index) => ( + {challengeList.map((challenge, index) => ( { src={FinishStamp} /> - - {challenge.title} - + + {challenge.title} + + { sessionStorage.setItem('activeTab', '0'); // 선택 탭 초기화 - const [listChallenges, setListChallenges] = useState([]); + const [challengeList, setChallengeList] = useState([]); useEffect(() => { const fetchCurrentChallenges = async () => { try { - const challenges = await getCurrentChallengeList(0, 10); - setListChallenges(challenges.data.data); - console.log(challenges.data.data); + const response = await getCurrentChallengeList(0, 10); + setChallengeList(response.data.data); + console.log(response.data.data); } catch (error) { console.error('Error fetching challenges:', error); } @@ -35,7 +35,7 @@ const MyChallengePage = () => { 참여 중인 챌린지 - + @@ -45,7 +45,7 @@ const MyChallengePage = () => { export default MyChallengePage; const MyChallengeLayout = styled(Box)` - min-height: calc(100vh - ${HEADER_HEIGHT}); + min-height: calc(100vh - ${HEADER_HEIGHT} - ${NAVBAR_HEIGHT}); display: flex; flex-direction: column; `;