diff --git a/README.md b/README.md index 380abd2..ffa39ef 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,13 @@ https://zzansuni-fe-vercel.vercel.app/ 사용자들에게 챌린지에 대해 공유 할 수 있으며, 랭킹을 통해 서로 경쟁할 수 있습니다. +### 개발자 소개 + +| 백엔드 | 백엔드 | 프론트엔드 | 프론트엔드 | 백엔드 | +| :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | +| [](https://github.com/momnpa333) | [](https://github.com/kwonssshyeon) | [](https://github.com/Dobbymin) | [](https://github.com/joojjang) | [](https://github.com/bayy1216) | +| 권다운 | 권수현 | 김강민 | 김민주 | 손홍석 | + ### 개발 동기 기획 단계에서 팀원들과 다양한 아이디어에 대해 생각을 해 보았고, 공통적으로 @@ -27,3 +34,20 @@ https://zzansuni-fe-vercel.vercel.app/ 그 결과 다양한 주제의 챌린지를 진행할 수 있는 '짠순이' 라는 서비스를 기획하게 되었습니다. +## 기술 스택 + +- Frontend: `React`, `vite`, `typescript`, `@emotion/styled`, `@emotion/react` +- Backend: `Spring`, `JPA`, `MySQL`, `S3` + +## 아키텍쳐 +![image](https://github.com/user-attachments/assets/e0bfee6d-4de3-448b-ae20-f78936a7074e) + + +## ERD + +![image](https://github.com/user-attachments/assets/8cd2aa03-9bd1-4ee0-a511-451711da40bb) + + +## CLASS DIAGRAM + +![image](https://github.com/user-attachments/assets/60e5ea66-eb87-4f9f-84bd-94eb2e70963e) 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/star-rating/index.tsx b/src/components/common/star-rating/index.tsx index 4717606..dc1a998 100644 --- a/src/components/common/star-rating/index.tsx +++ b/src/components/common/star-rating/index.tsx @@ -11,13 +11,16 @@ interface StarRatingProps { export const StarRating = ({ rating, size = 24, onClick }: StarRatingProps) => { const [ratingToPercent, setRatingToPercent] = useState(0); + // rating 새로 전달받을 때마다 퍼센테이지 계산 useEffect(() => { if (rating !== undefined) { setRatingToPercent((rating / 5) * 100); } }, [rating]); - const handleClick = (rating: number) => { + // 별점 클릭 핸들러 + const handleClickStar = (rating: number, e: React.MouseEvent) => { + e.preventDefault(); if (onClick) { onClick(rating + 1); // 클릭한 별점 값 전달 (1부터 시작) } @@ -27,14 +30,20 @@ export const StarRating = ({ rating, size = 24, onClick }: StarRatingProps) => { {[...Array(5)].map((_, index) => ( - handleClick(index)}> + handleClickStar(index, e)} + > ★ ))} {[...Array(5)].map((_, index) => ( - handleClick(index)}> + handleClickStar(index, e)} + > ★ ))} diff --git a/src/components/common/form/textarea/tooltip/index.tsx b/src/components/common/tooltip/index.tsx similarity index 99% rename from src/components/common/form/textarea/tooltip/index.tsx rename to src/components/common/tooltip/index.tsx index ecd13ad..2058701 100644 --- a/src/components/common/form/textarea/tooltip/index.tsx +++ b/src/components/common/tooltip/index.tsx @@ -60,7 +60,7 @@ const TooltipBox = styled.div<{ direction: string }>` font-size: 12px; font-weight: 500; white-space: nowrap; - z-index: 10; + z-index: 5; display: flex; align-items: center; diff --git a/src/components/features/layout/nav-bar/index.tsx b/src/components/features/layout/nav-bar/index.tsx index 28e23f8..9289dc7 100644 --- a/src/components/features/layout/nav-bar/index.tsx +++ b/src/components/features/layout/nav-bar/index.tsx @@ -16,7 +16,7 @@ const NavBar = () => { return ( {navBarData.map((item) => ( - handleNav(item.path)}> + handleNav(item.path)}> ))} diff --git a/src/components/features/layout/top-bar/page-bar.tsx b/src/components/features/layout/top-bar/page-bar.tsx index 7af2507..1398f40 100644 --- a/src/components/features/layout/top-bar/page-bar.tsx +++ b/src/components/features/layout/top-bar/page-bar.tsx @@ -60,7 +60,7 @@ const PageBarLayout = styled(Box)<{ padding: 0.5rem; gap: 1rem; background-color: ${(props) => props.backgroundColor}; - z-index: 1; + z-index: 10; position: sticky; top: ${({ show }) => (show ? '0' : '-100px')}; transition: top 0.3s; diff --git a/src/pages/challenge-record/records/index.tsx b/src/pages/challenge-record/records/index.tsx index b4abdeb..2ca9cda 100644 --- a/src/pages/challenge-record/records/index.tsx +++ b/src/pages/challenge-record/records/index.tsx @@ -9,7 +9,7 @@ import { ChallengeRecordData, ChallengeRecordDetailData, } from '@/apis/challenge-record/challenge.record.response'; -import Tooltip from '@/components/common/form/textarea/tooltip'; +import Tooltip from '@/components/common/tooltip'; import { formatDate } from '@/utils/formatters'; import { Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; diff --git a/src/pages/my-challenge-record/components/list-item.tsx b/src/pages/my-challenge-record/components/list-item.tsx index 6da7be0..0d0523f 100644 --- a/src/pages/my-challenge-record/components/list-item.tsx +++ b/src/pages/my-challenge-record/components/list-item.tsx @@ -9,12 +9,10 @@ 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(); @@ -51,11 +49,7 @@ const ListItem = ({ challengeId, challengeTitle, profileImageUrl }: Props) => { return ( - profile + profile handleChallengeClick(challengeId, challengeTitle)} diff --git a/src/pages/my-challenge-record/index.tsx b/src/pages/my-challenge-record/index.tsx index 9051c5d..91243e1 100644 --- a/src/pages/my-challenge-record/index.tsx +++ b/src/pages/my-challenge-record/index.tsx @@ -1,8 +1,8 @@ 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 TopBar, { HEADER_HEIGHT } from '@/components/features/layout/top-bar'; import { Box, Spinner } from '@chakra-ui/react'; @@ -11,7 +11,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,11 +54,9 @@ const MyChallengeRecord = () => { {allChallenges.length > 0 ? ( allChallenges.map((challenge, index) => ( )) ) : ( diff --git a/src/pages/rank/components/all/index.tsx b/src/pages/rank/components/all/index.tsx index 0f2f280..6054a9d 100644 --- a/src/pages/rank/components/all/index.tsx +++ b/src/pages/rank/components/all/index.tsx @@ -16,7 +16,7 @@ const AllRank = () => { useEffect(() => { const fetchUserRanking = async () => { - const response: UserRankingResponse = await getUserRanking(1, 10); + const response: UserRankingResponse = await getUserRanking(0, 10); const pageData = response.data; const allUserData: User[] = pageData.data.map((user: UserData) => ({ ...user, diff --git a/src/pages/review-write/index.tsx b/src/pages/review-write/index.tsx index 401d663..51e36ee 100644 --- a/src/pages/review-write/index.tsx +++ b/src/pages/review-write/index.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { postReview } from '@/apis/review/review.api'; import ChallengeTitle from '@/components/common/challenge-title'; @@ -6,6 +7,7 @@ import CTA, { CTAContainer } from '@/components/common/cta'; import Textarea from '@/components/common/form/textarea'; import { StarRating } from '@/components/common/star-rating'; import TopBar, { HEADER_HEIGHT } from '@/components/features/layout/top-bar'; +import { RouterPath } from '@/routes/path'; import { formatRating, formatDifficulty, @@ -35,6 +37,7 @@ const ReviewWrite = () => { const [content, setContent] = useState(''); const [isContentValid, setIsContentValid] = useState(true); const [isButtonDisabled, setIsButtonDisabled] = useState(true); + const navigate = useNavigate(); const handleDifficultyClick = (difficulty: number) => { setSelectedDifficulty(difficulty); @@ -83,6 +86,7 @@ const ReviewWrite = () => { }) .then(() => { alert('리뷰가 등록되었습니다!'); + navigate(`/${RouterPath.challenge}/${RouterPath.myRecord}`); }) .catch((error) => { // API에서 받은 오류 객체일 경우 @@ -242,6 +246,8 @@ const Chip = styled.button<{ isSelected: boolean }>` font-size: var(--font-size-sm); font-weight: 600; text-align: center; + cursor: pointer; + outline: none; ${({ isSelected }) => isSelected && `