diff --git a/README.md b/README.md
index 2573f1c..af4989c 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) |
+| 권다운 | 권수현 | 김강민 | 김민주 | 손홍석 |
+
### 개발 동기
기획 단계에서 팀원들과 다양한 아이디어에 대해 생각을 해 보았고, 공통적으로
diff --git a/package.json b/package.json
index 89bc908..47a2e72 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --host",
"build": "tsc && vite build",
- "port": "vite --host 0.0.0.0 --port 5173",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
diff --git a/src/apis/challenge-record/challenge.record.api.ts b/src/apis/challenge-record/challenge.record.api.ts
index c6578e7..00b3121 100644
--- a/src/apis/challenge-record/challenge.record.api.ts
+++ b/src/apis/challenge-record/challenge.record.api.ts
@@ -1,3 +1,5 @@
+import { AxiosError } from 'axios';
+
import { multiPartClient, axiosClient } from '../AxiosClient';
import {
ApiResponse,
@@ -9,34 +11,42 @@ import {
// POST: /api/challenges/{challengeId}/verification
export async function postVerification(
- id: number,
- image: File,
- content: string
-): Promise {
- const formData = new FormData();
- formData.append(
+ challengeId: number,
+ content: string,
+ image: File
+): Promise {
+ const requestBody = new FormData();
+ requestBody.append(
'body',
new Blob([JSON.stringify({ content })], { type: 'application/json' })
);
- formData.append('image', image);
+ requestBody.append('image', image);
const response = await multiPartClient.post(
- `api/challenges/${id}/verifications`,
- formData
+ `api/challenges/${challengeId}/verification`,
+ requestBody
);
- console.log('postVerification response: ', response.data);
- return response.data;
+ console.log('postVerification response: ', response.data); // test
}
-// GET: /api/challenges/{challengeId}/record
export async function getChallengeRecord(
- id: number
+ challengeId: number
): Promise {
- const response = await axiosClient.get>(
- `api/challenges/${id}/record`
- );
- console.log('getChallengeRecord response: ', response.data);
- return response.data.data;
+ try {
+ const response = await axiosClient.get(
+ `api/challenges/${challengeId}/record`
+ );
+ // console.log('getChallengeRecord response: ', response.data); // test
+ return response.data.data;
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ throw new Error(
+ `getChallengeRecord error: ${error.response?.data.message || error.message}`
+ );
+ } else {
+ throw new Error('getChallengeRecord error: unexpected');
+ }
+ }
}
// GET: /api/challenges/record/{recordId}
@@ -50,11 +60,22 @@ export async function getChallengeRecordDetailorigin(
return response.data.data;
}
-export async function getChallengeRecordDetail(
+export async function getChallengeRecordDetails(
recordId: number
): Promise {
- const response = await axiosClient.get(`api/challenges/record/${recordId}`);
- return response.data;
+ try {
+ const response = await axiosClient.get(`api/challenges/record/${recordId}`);
+ // console.log('getChallengeRecordDetails response: ', response.data); // test
+ return response.data.data;
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ throw new Error(
+ `getChallengeRecord error: ${error.response?.data.message || error.message}`
+ );
+ } else {
+ throw new Error('getChallengeRecord error: unexpected');
+ }
+ }
}
// GET: /api/user/challenges/completes
diff --git a/src/apis/challenge-record/challenge.record.response.ts b/src/apis/challenge-record/challenge.record.response.ts
index edf347f..8273a39 100644
--- a/src/apis/challenge-record/challenge.record.response.ts
+++ b/src/apis/challenge-record/challenge.record.response.ts
@@ -6,29 +6,19 @@ export type ApiResponse = {
};
export type ChallengeRecordData = {
- result: string;
- data: {
- title: string;
- totalCount: number;
- successCount: number;
- startDate: string;
- endDate: string;
- recordIds: number[];
- };
- message: string;
- errorCode: string;
+ title: string;
+ totalCount: number;
+ successCount: number;
+ startDate: string;
+ endDate: string;
+ recordIds: number[];
};
export type ChallengeRecordDetailData = {
- result: string;
- data: {
- id: 0;
- createdAt: string;
- content: string;
- imageUrl: string;
- };
- message: string;
- errorCode: string;
+ id: 0;
+ createdAt: string;
+ content: string;
+ imageUrl: string;
};
export type CompleteChallengeData = {
diff --git a/src/apis/recently-review/getRecentlyReview.api.ts b/src/apis/recently-review/getRecentlyReview.api.ts
new file mode 100644
index 0000000..ff46c42
--- /dev/null
+++ b/src/apis/recently-review/getRecentlyReview.api.ts
@@ -0,0 +1,27 @@
+import { RecentlyReviewResponse } from './getRecentlyReview.response';
+import { axiosClient } from '@/apis/AxiosClient';
+import { useQuery } from '@tanstack/react-query';
+
+const getRecentlyReviewPath = () => '/api/challengeGroups/shorts';
+
+const recentlyReviewQueryKey = [getRecentlyReviewPath()];
+
+const getRecentlyReview = async (
+ page: number,
+ size: number
+): Promise => {
+ const response = await axiosClient.get(getRecentlyReviewPath(), {
+ params: {
+ page,
+ size,
+ },
+ });
+ return response.data;
+};
+
+export const useGetRecentlyReview = (page: number, size: number) => {
+ return useQuery({
+ queryKey: [recentlyReviewQueryKey, page, size],
+ queryFn: () => getRecentlyReview(page, size),
+ });
+};
diff --git a/src/apis/recently-review/getRecentlyReview.response.ts b/src/apis/recently-review/getRecentlyReview.response.ts
new file mode 100644
index 0000000..3d69449
--- /dev/null
+++ b/src/apis/recently-review/getRecentlyReview.response.ts
@@ -0,0 +1,24 @@
+import ApiResponse from '@/apis/ApiResponse';
+
+export type RecentlyReviewResponse = {
+ totalPage: number;
+ hasNext: boolean;
+ data: {
+ challengeId: number;
+ challengeTitle: string;
+ user: {
+ id: number;
+ nickname: string;
+ profileImageUrl: string;
+ tierInfo: {
+ tier: string;
+ totalExp: number;
+ currentExp: number;
+ };
+ };
+ content: string;
+ rating: number;
+ }[];
+};
+
+export type ChallengeListResponse = ApiResponse;
diff --git a/src/assets/shorts/shorts-img.png b/src/assets/shorts/shorts-img.png
new file mode 100644
index 0000000..32fd298
Binary files /dev/null and b/src/assets/shorts/shorts-img.png differ
diff --git a/src/assets/stamp-active.svg b/src/assets/stamp-active.svg
new file mode 100644
index 0000000..5e72aaf
--- /dev/null
+++ b/src/assets/stamp-active.svg
@@ -0,0 +1,27 @@
+
diff --git a/src/assets/stamp-inactive.svg b/src/assets/stamp-inactive.svg
new file mode 100644
index 0000000..0015328
--- /dev/null
+++ b/src/assets/stamp-inactive.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/components/common/challenge-title/index.tsx b/src/components/common/challenge-title/index.tsx
new file mode 100644
index 0000000..380d529
--- /dev/null
+++ b/src/components/common/challenge-title/index.tsx
@@ -0,0 +1,29 @@
+import { Text } from '@chakra-ui/react';
+import styled from '@emotion/styled';
+
+type ChallengeTitleProps = {
+ category: string;
+ title: string;
+};
+
+const ChallengeTitle = ({ category, title }: ChallengeTitleProps) => {
+ return (
+
+
+ {category}
+
+
+ {title}
+
+
+ );
+};
+
+export default ChallengeTitle;
+
+const Wrapper = styled.div`
+ margin: 16px;
+ display: flex;
+ flex-direction: column;
+ text-align: left;
+`;
diff --git a/src/components/common/cta/index.tsx b/src/components/common/cta/index.tsx
index ca8ad21..dbffe59 100644
--- a/src/components/common/cta/index.tsx
+++ b/src/components/common/cta/index.tsx
@@ -16,6 +16,8 @@ const CTA = ({ label, disabled, onClick }: CTAProps) => {
export default CTA;
+export const CTA_CONTAINER_HEIGHT = '4rem';
+
const StyledCTA = styled.button<{ disabled?: boolean }>`
width: calc(100% - 16px); // 부모 요소의 좌우 padding 빼고
border: none;
@@ -53,7 +55,8 @@ export const CTAContainer = styled.div`
bottom: 0;
display: flex;
width: 100%;
- height: 4rem;
+ height: ${CTA_CONTAINER_HEIGHT};
padding: 8px 16px;
background-color: var(--color-white);
+ z-index: 1;
`;
diff --git a/src/components/common/form/textarea/index.tsx b/src/components/common/form/textarea/index.tsx
index c2b1b2d..08716e4 100644
--- a/src/components/common/form/textarea/index.tsx
+++ b/src/components/common/form/textarea/index.tsx
@@ -1,20 +1,41 @@
+import { Text } from '@chakra-ui/react';
import styled from '@emotion/styled';
type TextareaProps = {
placeholder?: string;
value: string;
onChange: (e: React.ChangeEvent) => void;
+ minValueLength?: number;
valid?: boolean;
};
-const Textarea = ({ placeholder, value, onChange, valid }: TextareaProps) => {
+const Textarea = ({
+ placeholder,
+ value,
+ onChange,
+ minValueLength,
+ valid,
+}: TextareaProps) => {
return (
-
+ <>
+
+ {/* 최소 길이 제한 있을 때만 보이기 */}
+ {minValueLength && (
+
+ {value.length} / 최소 {minValueLength}자
+
+ )}
+ >
);
};
@@ -29,10 +50,10 @@ const StyledTextarea = styled.textarea<{ valid?: boolean }>`
? 'var(--color-grey-02) 1px solid'
: 'var(--color-class-05) 1px solid'};
padding: 12px;
- width: 100%;
height: 180px;
resize: none;
outline: none;
+ margin: 0 16px;
&::placeholder {
color: var(--color-grey-01);
diff --git a/src/components/common/tabs/tab-panels/index.tsx b/src/components/common/tabs/tab-panels/index.tsx
index 220e035..a2f3564 100644
--- a/src/components/common/tabs/tab-panels/index.tsx
+++ b/src/components/common/tabs/tab-panels/index.tsx
@@ -27,19 +27,20 @@ export const TabPanel = ({ children, value, selectedIndex }: TabPanelProps) => {
};
export const StyledTabPanels = styled.div`
- height: 100%;
width: 100%;
position: relative;
text-align: center;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
`;
export const StyledTabPanel = styled.div<{
active: boolean;
}>`
display: ${(p) => (p.active ? 'flex' : 'none')};
- font-size: 2rem;
flex-direction: column;
+ flex: 1;
width: 100%;
- height: 100%;
- justify-content: center;
+ font-size: 2rem;
`;
diff --git a/src/components/features/layout/nav-bar/index.tsx b/src/components/features/layout/nav-bar/index.tsx
index 03dffe0..9289dc7 100644
--- a/src/components/features/layout/nav-bar/index.tsx
+++ b/src/components/features/layout/nav-bar/index.tsx
@@ -1,7 +1,7 @@
import { useNavigate } from 'react-router-dom';
import { navBarData } from '@/constants/nav-bar';
-import { Box } from '@chakra-ui/react';
+import { Box, Image } from '@chakra-ui/react';
import styled from '@emotion/styled';
export const NAVBAR_HEIGHT = '3.44rem';
@@ -14,14 +14,10 @@ const NavBar = () => {
};
return (
-
+
{navBarData.map((item) => (
-
- handleNav(item.path)}
- />
+ handleNav(item.path)}>
+
))}
@@ -44,20 +40,17 @@ const Wrapper = styled(Box)`
background-color: #fafafa;
`;
-const Tab = styled.div`
- width: 50%;
+const Tab = styled.a`
+ width: 50%; // 요소마다 부모 요소의 너비를 균등하게 차지하도록
height: 100%;
- display: inline-flex;
+ display: flex;
justify-content: center;
align-items: center;
+ cursor: pointer;
`;
-const Icon = styled.button<{ src: string }>`
+const IconImage = styled(Image)`
width: 2rem;
height: 2rem;
outline: none;
- background-image: url(${({ src }) => src});
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
`;
diff --git a/src/components/features/layout/top-bar/page-bar.tsx b/src/components/features/layout/top-bar/page-bar.tsx
index 3020a30..7af2507 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: 1000;
+ z-index: 1;
position: sticky;
top: ${({ show }) => (show ? '0' : '-100px')};
transition: top 0.3s;
diff --git a/src/pages/challenge-detail/index.tsx b/src/pages/challenge-detail/index.tsx
index 3652cbb..d3961ee 100644
--- a/src/pages/challenge-detail/index.tsx
+++ b/src/pages/challenge-detail/index.tsx
@@ -7,6 +7,7 @@ import { RankingSection } from './ranking-section/';
import { ReviewSection } from './review-section/';
import { type ChallengeDetailData } from '@/apis/challenge-detail/challenge.detail.response';
import DefaultImage from '@/assets/Default-Image.svg';
+import ChallengeTitle from '@/components/common/challenge-title';
import { Tabs, Tab } from '@/components/common/tabs';
import { TabPanels, TabPanel } from '@/components/common/tabs/tab-panels';
import TopBar from '@/components/features/layout/top-bar';
@@ -79,12 +80,12 @@ const ChallengeDetailPage = () => {
)}
-
-
- {formatCategory(data?.category)}
- {data?.title}
-
-
+ {data && (
+
+ )}
{tabsList.map((t, index) => (
@@ -140,20 +141,3 @@ export const StyledDefaultImage = styled.img`
object-fit: cover;
opacity: 50%;
`;
-
-export const ChallengeTitleWrapper = styled.div`
- margin: 16px;
- display: flex;
- flex-direction: column;
- text-align: left;
-`;
-
-export const Category = styled.div`
- font-size: var(--font-size-xs);
- color: var(--color-green-01);
-`;
-
-export const Title = styled.div`
- font-size: var(--font-size-xl);
- font-weight: bold;
-`;
diff --git a/src/pages/challenge-list/components/contents/index.tsx b/src/pages/challenge-list/components/contents/index.tsx
index 2aa0b64..b9e4f11 100644
--- a/src/pages/challenge-list/components/contents/index.tsx
+++ b/src/pages/challenge-list/components/contents/index.tsx
@@ -9,6 +9,8 @@ type Props = {
startDate: string;
endDate: string;
participantCount: number;
+ id: number;
+ onClick: (id: number) => void;
};
const Contents = ({
@@ -17,11 +19,14 @@ const Contents = ({
startDate,
endDate,
participantCount,
+ onClick,
+ id,
}: Props) => {
const [isClicked, setIsClicked] = useState(false);
const handleBoxClick = () => {
setIsClicked(!isClicked);
+ onClick(id);
};
const date = `${startDate} ~ ${endDate}`;
diff --git a/src/pages/challenge-list/index.tsx b/src/pages/challenge-list/index.tsx
index 4ea2249..8d0da1c 100644
--- a/src/pages/challenge-list/index.tsx
+++ b/src/pages/challenge-list/index.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import Contents from './components/contents';
import { useGetChallengeList } from '@/apis/challenge-list/getChallengeList.api';
@@ -23,6 +24,12 @@ const ChallengeList = () => {
const [allData, setAllData] = useState([]);
const [page, setPage] = useState(0);
+ const navigate = useNavigate();
+
+ const handleNavigate = (id: number) => {
+ navigate(`/challenge/${id}`);
+ };
+
const categoryList = useMemo(
() => [
{ label: '에코', data: 'ECHO' },
@@ -102,6 +109,8 @@ const ChallengeList = () => {
startDate={challenge.startDate}
endDate={challenge.endDate}
participantCount={challenge.participantCount}
+ onClick={() => handleNavigate(challenge.id)}
+ id={challenge.id}
/>
))}
>
diff --git a/src/pages/challenge-record/components/bottom-sheet/index.tsx b/src/pages/challenge-record/components/bottom-sheet/index.tsx
deleted file mode 100644
index 8394b4b..0000000
--- a/src/pages/challenge-record/components/bottom-sheet/index.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { motion, PanInfo } from 'framer-motion';
-
-import useBottomSheet from '@/hooks/useBottomSheet';
-import { Box, Image } from '@chakra-ui/react';
-import styled from '@emotion/styled';
-
-type Props = {
- data: {
- id: 0;
- createdAt: string;
- content: string;
- imageUrl: string;
- } | null;
- isOpen: boolean;
- onDragEnd: (
- event: MouseEvent | TouchEvent | PointerEvent,
- info: PanInfo
- ) => void;
-};
-
-const BottomSheet = ({ data, isOpen, onDragEnd }: Props) => {
- const { controls } = useBottomSheet(isOpen);
-
- if (!isOpen || !data) {
- return null;
- }
-
- return (
-
-
-
- {data.createdAt.substr(0, 10)}
-
-
-
- {data.content}
-
-
-
-
- );
-};
-
-export default BottomSheet;
-
-const BottomSheetBox = styled(Box)`
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 100;
-`;
-
-const Wrapper = styled(motion.div)`
- z-index: 101;
- flex-direction: column;
- position: fixed;
- left: 0;
- right: 0;
- bottom: -50px;
- border-top-left-radius: 20px;
- border-top-right-radius: 20px;
- background-color: var(--color-green-06);
- box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.6);
- height: 70%;
- width: 100%;
- transition: transform 150ms ease-out;
- margin: 0 auto;
- overflow: auto;
-`;
-
-const HeaderWrapper = styled(motion.div)`
- z-index: 102;
- height: 20px;
- border-top-left-radius: 20px;
- border-top-right-radius: 20px;
- position: relative;
- padding: 15px;
- background-color: var(--color-green-01);
- color: var(--color-white);
- display: flex;
- flex-direction: row;
-`;
-
-const HandleBar = styled(motion.div)`
- z-index: 103;
- position: absolute;
- left: 50%;
- margin-left: -16px;
- width: 32px;
- height: 2px;
- border-radius: 2px;
- background-color: var(--color-white);
-`;
-
-const Text = styled.div`
- font-size: var(--font-size-lg);
- align-self: center;
-`;
-
-const ContentWrapper = styled.div`
- padding: 20px;
- display: flex;
- flex-direction: column;
- gap: 20px;
-`;
-
-const SubText = styled.div`
- font-size: var(--font-size-md);
- text-align: left;
-`;
diff --git a/src/pages/challenge-record/components/caution/index.tsx b/src/pages/challenge-record/components/caution/index.tsx
new file mode 100644
index 0000000..faa585d
--- /dev/null
+++ b/src/pages/challenge-record/components/caution/index.tsx
@@ -0,0 +1,43 @@
+import { Text } from '@chakra-ui/react';
+import styled from '@emotion/styled';
+
+const Caution = () => {
+ return (
+
+
+ 유의 사항
+
+
+ 모든 스탬프를 모으면 챌린지를 완수하게 됩니다.
+
+ 스탬프는 하루 1개로 제한됩니다. (동일 챌린지를 하루에 여러 번
+ 인증하더라도 1회만 인정됩니다.)
+
+
+ 명시된 횟수를 초과한 경우 챌린지 완수로 인정되나 추가 인증에 대한
+ 스탬프나 포인트는 제공되지 않습니다.
+
+
+ 사진 조작, 타인의 계정 이용 등의 부정 행위 적발 시 해당 계정은 강제
+ 탈퇴되며 추후 서비스 이용에 제한이 있을 수 있습니다.
+
+ 스탬프가 정상 인증되지 않는 경우 운영 측으로 문의해주세요.
+
+
+ );
+};
+
+export default Caution;
+
+const Wrapper = styled.div`
+ padding: 24px 16px;
+ background-color: var(--color-class-01);
+ color: var(--color-black);
+ text-align: left;
+ margin-top: auto;
+`;
diff --git a/src/pages/challenge-record/components/record-item/index.tsx b/src/pages/challenge-record/components/record-item/index.tsx
new file mode 100644
index 0000000..cdddcac
--- /dev/null
+++ b/src/pages/challenge-record/components/record-item/index.tsx
@@ -0,0 +1,145 @@
+import { motion, PanInfo } from 'framer-motion';
+
+import { ChallengeRecordDetailData } from '@/apis/challenge-record/challenge.record.response';
+import { HEADER_HEIGHT } from '@/components/features/layout/top-bar';
+import useBottomSheet from '@/hooks/useBottomSheet';
+import { formatDate } from '@/utils/formatters';
+import { Image, Text } from '@chakra-ui/react';
+import styled from '@emotion/styled';
+
+type RecordItemProps = {
+ data: ChallengeRecordDetailData;
+ recordIndex: number;
+ isOpen: boolean;
+ onDragEnd: (
+ event: MouseEvent | TouchEvent | PointerEvent,
+ info: PanInfo
+ ) => void;
+};
+
+const RecordItem = ({
+ data,
+ recordIndex,
+ isOpen,
+ onDragEnd,
+}: RecordItemProps) => {
+ const { controls } = useBottomSheet(isOpen);
+
+ if (!isOpen || !data) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {recordIndex}회차 인증
+
+
+ {formatDate(data.createdAt)}
+
+ {data.content}
+
+
+
+
+ );
+};
+
+export default RecordItem;
+
+const Wrapper = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: flex-end; // 자식이 하단에 붙게
+ z-index: 100;
+ overflow: hidden;
+`;
+
+const Inner = styled(motion.div)`
+ z-index: 101;
+ width: 100%;
+ min-height: 60%;
+ max-height: calc(100vh - ${HEADER_HEIGHT});
+ overflow-y: auto;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 20px 20px 0 0;
+ background-color: var(--color-green-06);
+ transition: transform 150ms ease-out;
+ margin: 0 auto;
+`;
+
+const Handle = styled(motion.div)`
+ z-index: 102;
+ height: 36px;
+ position: relative;
+ padding: 16px;
+ background-color: var(--color-green-01);
+ color: var(--color-white);
+`;
+
+const HandleBar = styled(motion.div)`
+ z-index: 103;
+ position: absolute;
+ top: 10px;
+ transform: translateX(-50%);
+ left: 50%;
+ width: 40px;
+ height: 4px;
+ border-radius: 10px;
+ background-color: var(--color-green-06);
+`;
+
+const Content = styled.div`
+ padding: 12px 16px 16px 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ position: relative;
+`;
diff --git a/src/pages/challenge-record/components/stamp-board/index.tsx b/src/pages/challenge-record/components/stamp-board/index.tsx
deleted file mode 100644
index f03a1cb..0000000
--- a/src/pages/challenge-record/components/stamp-board/index.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import { useState, useEffect } from 'react';
-
-import BottomSheet from '../bottom-sheet';
-import Stamp from '../stamp';
-import {
- getChallengeRecord,
- getChallengeRecordDetail,
-} from '@/apis/challenge-record/challenge.record.api';
-import {
- ChallengeRecordData,
- ChallengeRecordDetailData,
-} from '@/apis/challenge-record/challenge.record.response';
-import { Box, Text } from '@chakra-ui/react';
-import styled from '@emotion/styled';
-
-const StampBoard = () => {
- const [isBottomSheetOpen, setBottomSheetOpen] = useState(false);
- const [data, setData] = useState(null);
- const [items, setItems] = useState([]);
- const [record, setRecord] = useState<
- ChallengeRecordDetailData['data'] | null
- >(null);
-
- const setArray = (length: number, values: number[]) => {
- const array = new Array(length).fill(-1);
- for (let i = 0; i < values.length; i++) {
- array[i] = values[i];
- }
- setItems(array);
- };
-
- const chunkItems = (arr: number[], chunkSize: number): number[][] => {
- const chunks = [];
- const last = arr.length % chunkSize === 1 ? chunkSize + 1 : 0;
- for (let i = 0; i < arr.length - last; i += chunkSize)
- chunks.push(arr.slice(i, i + chunkSize));
-
- for (let i = arr.length - last; i < arr.length; i += chunkSize - 1)
- chunks.push(arr.slice(i, i + (chunkSize - 1)));
- return chunks;
- };
-
- const rows = chunkItems(items, 3);
-
- const toggleBottomSheet = async (idx: number) => {
- if (idx === -1) {
- setBottomSheetOpen(false);
- console.log('Closing BottomSheet');
- } else {
- try {
- const response = await getChallengeRecordDetail(idx);
- setRecord(response.data);
- setBottomSheetOpen(true);
- } catch (error) {
- console.error('Failed to fetch challenge record detail:', error);
- }
- }
- };
-
- const handleDragEnd = (
- _event: MouseEvent | TouchEvent | PointerEvent,
- info: { offset: { x: number; y: number } }
- ) => {
- if (info.offset.y > 100) {
- setBottomSheetOpen(false);
- }
- };
-
- useEffect(() => {
- const fetchChallengeRecord = async () => {
- try {
- const response = await getChallengeRecord(18);
- setData(response.data);
- setArray(response.data.totalCount, response.data.recordIds);
- } catch (error) {
- console.error('Failed to fetch challenge record:', error);
- }
- };
-
- fetchChallengeRecord();
- }, []);
-
- return (
- <>
- {data ? (
-
-
- {data.title}
-
-
-
- 챌린지 인증하고 레벨업!
-
- 짠돌이가 응원해요
-
-
- 챌린지 유효기간 {data.startDate} ~ {data.endDate}
-
- {rows.map((row, rowIndex) => (
-
- {row.map((item, index) => (
- - {
- toggleBottomSheet(item);
- }}
- >
-
-
- ))}
-
- ))}
-
-
- ) : (
-
- )}
-
-
-
- 유의사항
-
-
- 별도의 규칙이 없는 한 스탬프는 하루 한개로 제한됩니다. (동일 챌린지에
- 하루 여러건 참여한 것은 인정되지 않습니다.)
-
- 모든 스탬프를 찍은 후 챌린지가 완료되며, 일부만 수행한 경우 챌린지가
- 완료로 표시되지 않습니다.
-
- 명시된 횟수를 초과한 경우 챌린지 완료로 인정되나 추가 인증에 대한
- 포인트는 제공되지 않습니다.
-
- 사진 조작, 타인의 계정 이용등의 부정행위가 적발될 시 해당 계정은 자동
- 탈퇴되며 추후 서비스 이용에 제한이 있을 수 있습니다.
-
- 스탬프가 정상 인증되지 않는경우 고객센터로 문의하세요
-
-
-
-
-
- >
- );
-};
-
-export default StampBoard;
-
-const StampBoardBox = styled(Box)`
- margin: 30px;
- margin-bottom: 50px;
- display: flex;
- flex-direction: column;
- text-align: left;
- overflow-y: auto;
- scrollbar-color: transparent transparent;
- &::-webkit-scrollbar {
- display: none;
- }
-`;
-
-const StampBox = styled.div`
- border-radius: 20px;
- text-align: center;
- padding: 10px 20px;
- background-color: var(--color-green-06);
- margin-bottom: calc(100% - 250px);
-`;
-
-const CautionWrapper = styled.div`
- margin-bottom: 50px;
- padding: 25px;
- background-color: var(--color-grey-01);
- color: var(--color-white);
- text-align: left;
-`;
-
-const Item = styled.div<{ rowLength: number }>`
- width: 100%;
- flex: 1;
- text-align: center;
-
- ${({ rowLength }) =>
- rowLength === 1 &&
- `
- flex: 1 1 100%;
- `}
- ${({ rowLength }) =>
- rowLength === 2 &&
- `
- flex: 1 1 calc(50% - 10px);
- `}
- ${({ rowLength }) =>
- rowLength === 3 &&
- `
- flex: 1 1 calc(33.33% - 20px);
- `}
-`;
diff --git a/src/pages/challenge-record/components/stamp/index.tsx b/src/pages/challenge-record/components/stamp/index.tsx
index 1ce9cc6..607665c 100644
--- a/src/pages/challenge-record/components/stamp/index.tsx
+++ b/src/pages/challenge-record/components/stamp/index.tsx
@@ -1,19 +1,22 @@
-import StampActive from '@/assets/StampActive.svg';
-import StampInActive from '@/assets/StampInactive.svg';
+import StampActive from '@/assets/stamp-active.svg';
+import StampInActive from '@/assets/stamp-inactive.svg';
import { Image } from '@chakra-ui/react';
import styled from '@emotion/styled';
type StampProps = {
- data: number;
+ id: number;
+ onClick?: () => void;
};
-const Stamp = ({ data }: StampProps) => {
+const Stamp = ({ id, onClick }: StampProps) => {
+ const active = id !== -1 ? true : false; // -1이면 inactive
+
return (
<>
- {data === -1 ? (
-
+ {active ? (
+
) : (
-
+
)}
>
);
@@ -21,8 +24,7 @@ const Stamp = ({ data }: StampProps) => {
export default Stamp;
-const StampImage = styled(Image)`
- width: 80px;
- height: 80px;
- margin: 5px auto;
+const StyledStamp = styled(Image)<{ active: boolean }>`
+ aspect-ratio: 1 / 1;
+ cursor: ${({ active }) => (active ? 'pointer' : null)};
`;
diff --git a/src/pages/challenge-record/components/verification/index.tsx b/src/pages/challenge-record/components/verification/index.tsx
deleted file mode 100644
index a00d96f..0000000
--- a/src/pages/challenge-record/components/verification/index.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import { useRef, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { postVerification } from '@/apis/challenge-record/challenge.record.api';
-import { RouterPath } from '@/routes/path';
-import { Image, Text } from '@chakra-ui/react';
-import styled from '@emotion/styled';
-
-const Verification = () => {
- const fileInput = useRef(null);
- const [previewImg, setPreviewImg] = useState(null);
- const [text, setText] = useState('');
- const navigate = useNavigate();
-
- const saveHandler = async () => {
- try {
- if (previewImg) {
- const response = await postVerification(18, previewImg, text);
- if (response.status === 200) {
- console.log('응답: ', response);
- alert('성공적으로 저장했습니다.');
- navigate(RouterPath.root);
- }
- } else {
- alert('이미지를 선택하세요.');
- }
- } catch (error) {
- alert('저장에 실패했습니다.');
- }
- };
-
- const handleButtonClick = () => {
- fileInput.current?.click();
- };
-
- const imageHandler = (fileBlob: File) => {
- setPreviewImg(fileBlob); // File 객체를 직접 설정합니다.
-
- // 미리보기를 위해 FileReader를 사용합니다.
- const reader = new FileReader();
- reader.readAsDataURL(fileBlob);
- reader.onload = () => {
- if (reader.result && typeof reader.result === 'string') {
- // 여기서 미리보기 이미지 URL을 설정합니다.
- const imgElement = document.querySelector(
- '#previewImage'
- ) as HTMLImageElement;
- if (imgElement) {
- imgElement.src = reader.result;
- }
- }
- };
- };
-
- return (
-
-
- 길에 떨어진 쓰레기 줍기 챌린지
-
- setText(e.target.value)}
- />
- {!previewImg && (
-
- {
- if (e.target.files) {
- imageHandler(e.target.files[0]);
- }
- }}
- />
- 사진추가
-
- )}
- {previewImg && }{' '}
- {/* 미리보기 이미지 */}
- 참여하기
-
- );
-};
-
-export default Verification;
-
-// Styled components
-const VerificationBox = styled.div`
- display: flex;
- margin: 2.5rem 0;
- flex-direction: column;
- text-align: left;
-
- overflow-y: auto;
- scrollbar-color: transparent transparent;
- &::-webkit-scrollbar {
- display: none;
- }
-`;
-
-const InputArea = styled.textarea`
- font-size: var(--font-size-sm);
- border-radius: 20px;
- border: var(--color-green-01) 1px solid;
- padding: 10px;
- height: 30vh;
- resize: none;
- :focus {
- outline: none;
- }
-`;
-
-const AddImageBtn = styled.div`
- border-radius: 20px;
- background-color: #fff;
- font-size: var(--font-size-md);
- color: var(--color-green-01);
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 45px;
- border: var(--color-green-01) 1px solid;
- margin-top: 30px;
-`;
-
-const SummitButton = styled.button`
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 1rem;
- height: 50px;
- margin-top: 30px;
- border-radius: 20px;
- background-color: var(--color-green-01);
- color: #fff;
- font-size: var(--font-size-md);
- font-weight: bold;
- border: none;
-`;
-
-const PreviewImage = styled(Image)`
- margin-top: 30px;
- margin-bottom: 60px;
- border-radius: 20px;
- width: 100%;
- object-fit: cover;
- border: none;
-`;
-
-export const ModalBack = styled.div`
- position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- background: rgba(0, 0, 0, 0.8);
-`;
-
-export const ModalBackDrop = styled.div`
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 60%;
- height: 20%;
- background-color: var(--color-white);
- border-radius: 10px;
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 100;
- box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
-`;
diff --git a/src/pages/challenge-record/index.tsx b/src/pages/challenge-record/index.tsx
index f9c0059..59bd32e 100644
--- a/src/pages/challenge-record/index.tsx
+++ b/src/pages/challenge-record/index.tsx
@@ -1,33 +1,43 @@
import { useState } from 'react';
-import StampBoard from './components/stamp-board';
-import Verification from './components/verification';
-import { Tabs, Tab } from '@/components/common/tabs';
-import { TabPanels, TabPanel } from '@/components/common/tabs/tab-panels';
-import TopBar from '@/components/features/layout/top-bar';
+import Records from './records';
+import Verification from './verification';
+import ChallengeTitle from '@/components/common/challenge-title';
+import { Tab, Tabs } from '@/components/common/tabs';
+import { TabPanel, TabPanels } from '@/components/common/tabs/tab-panels';
+import TopBar, { HEADER_HEIGHT } from '@/components/features/layout/top-bar';
import styled from '@emotion/styled';
+const SAMPLE_CATEGORY = '에코';
+const SAMPLE_TITLE = '환경 정화 활동';
+
const ChallengeRecord = () => {
- const [activeTab, setActiveTab] = useState(0);
+ const [activeTab, setActiveTab] = useState(() => {
+ // 세션 스토리지에 저장된 값 | 기본값 0
+ const savedTab = sessionStorage.getItem('activeTab');
+ return savedTab ? Number(savedTab) : 0;
+ });
const tabsList = [
{
label: '인증 기록',
- panel: ,
+ panel: ,
},
{
label: '인증하기',
- panel: ,
+ panel: ,
},
];
const handleSelectedTab = (value: number) => {
setActiveTab(value as 0 | 1);
+ sessionStorage.setItem('activeTab', String(value));
};
return (
<>
+
{tabsList.map((t, index) => (
@@ -48,6 +58,8 @@ const ChallengeRecord = () => {
export default ChallengeRecord;
const Wrapper = styled.div`
- display: block;
- width: 100%;
+ min-height: calc(100vh - ${HEADER_HEIGHT});
+ display: flex;
+ flex-direction: column;
+ flex: 1;
`;
diff --git a/src/pages/challenge-record/records/index.tsx b/src/pages/challenge-record/records/index.tsx
new file mode 100644
index 0000000..734f1f7
--- /dev/null
+++ b/src/pages/challenge-record/records/index.tsx
@@ -0,0 +1,195 @@
+import { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+
+import Caution from '../components/caution';
+import RecordItem from '../components/record-item';
+import Stamp from '../components/stamp';
+import {
+ getChallengeRecord,
+ getChallengeRecordDetails,
+} from '@/apis/challenge-record/challenge.record.api';
+import {
+ ChallengeRecordData,
+ ChallengeRecordDetailData,
+} from '@/apis/challenge-record/challenge.record.response';
+import { formatDate } from '@/utils/formatters';
+import { Text } from '@chakra-ui/react';
+import styled from '@emotion/styled';
+
+const Records = () => {
+ const { id } = useParams();
+ const challengeId = Number(id);
+
+ const [data, setData] = useState(); // api 응답 데이터 전체
+ const [recordIdList, setRecordIdList] = useState([]);
+ const [recordDetails, setRecordDetails] =
+ useState(); // 인증기록 상세
+ const [isRecordItemOpen, setIsRecordItemOpen] = useState(false);
+
+ // recordId를 recordList에 추가하는 함수
+ const fillRecordList = (length: number, values: number[]) => {
+ const newRecordIdList = new Array(length).fill(-1); // 모두 -1로 초기화
+ for (let i = 0; i < values.length; i++) {
+ newRecordIdList[i] = values[i]; // recordId로 바꾸기
+ }
+ setRecordIdList(newRecordIdList);
+ };
+
+ // 기록 리스트 페칭
+ useEffect(() => {
+ getChallengeRecord(challengeId)
+ .then((res) => {
+ setData(res);
+ fillRecordList(res.totalCount, res.recordIds);
+ })
+ .catch((error) => {
+ console.error('Error fetching records:', error);
+ });
+ }, [challengeId]);
+
+ // 각각의 인증기록을 펼치는 핸들러
+ const handleStampClick = (recordId: number) => {
+ if (recordId === -1) {
+ // Stamp 안에서 분기 처리 되어 있긴 한데.
+ setIsRecordItemOpen(false);
+ console.log('Closed the record item');
+ } else {
+ getChallengeRecordDetails(recordId)
+ .then((res) => {
+ setRecordDetails(res);
+ setIsRecordItemOpen(true);
+ })
+ .catch((error) => {
+ console.error('Error fetching record details:', error);
+ });
+ }
+ };
+
+ // 바텀시트 열렸을 때 overflow-y 숨기기
+ useEffect(() => {
+ if (isRecordItemOpen) {
+ document.body.style.overflowY = 'hidden';
+ } else {
+ document.body.style.overflowY = '';
+ }
+
+ // 컴포넌트 언마운트 시 기본 값으로 되돌리기
+ return () => {
+ document.body.style.overflowY = '';
+ };
+ }, [isRecordItemOpen]);
+
+ // 인증기록 (RecordItem) 닫는 핸들러
+ const handleDragEnd = (
+ _event: MouseEvent | TouchEvent | PointerEvent,
+ info: { offset: { x: number; y: number } }
+ ) => {
+ if (info.offset.y > 100) {
+ setIsRecordItemOpen(false);
+ }
+ };
+
+ return (
+
+
+
+ 챌린지 인증하고 레벨 업!
+
+ 짠돌이가 응원해요
+
+ {data && (
+ <>
+
+ {/* 참여 기간 */}
+ 참여 기간
+
+ {formatDate(data.startDate)} ~ {formatDate(data.endDate)}
+
+
+ {/* 인증 횟수 */}
+ 인증 횟수
+
+ {data.successCount}
+ / {data.totalCount}회
+
+
+
+
+ {recordIdList.map((recordId, index) => (
+ {
+ handleStampClick(recordId);
+ }}
+ />
+ ))}
+
+ >
+ )}
+
+
+ {recordDetails && (
+
+ )}
+
+
+
+ );
+};
+
+export default Records;
+
+const Wrapper = styled.div`
+ margin: 16px 0 0 0;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ scrollbar-color: transparent transparent;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const StampBoard = styled.div`
+ margin: 0 16px 32px;
+ padding: 24px 16px;
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ border-radius: 20px;
+ border: 1px solid var(--color-grey-02);
+ background-color: var(--color-white);
+`;
+
+const InfoGrid = styled.div`
+ align-self: center;
+ display: grid;
+ grid-template-columns: max-content max-content;
+ grid-gap: 0 16px;
+ margin: 0 0 24px 0;
+ line-height: 2;
+ font-size: var(--font-size-sm);
+
+ .highlight {
+ font-weight: 600;
+ color: var(--color-green-01);
+ }
+`;
+
+const StampGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px 0;
+ justify-items: center;
+ align-items: center;
+`;
diff --git a/src/pages/challenge-record/verification/index.tsx b/src/pages/challenge-record/verification/index.tsx
new file mode 100644
index 0000000..6710a8f
--- /dev/null
+++ b/src/pages/challenge-record/verification/index.tsx
@@ -0,0 +1,193 @@
+import { useEffect, useRef, useState } from 'react';
+import { MdDeleteForever } from 'react-icons/md';
+import { useParams } from 'react-router-dom';
+
+import Caution from '../components/caution';
+import { postVerification } from '@/apis/challenge-record/challenge.record.api';
+import CTA, { CTAContainer } from '@/components/common/cta';
+import Textarea from '@/components/common/form/textarea';
+import { Image } from '@chakra-ui/react';
+import styled from '@emotion/styled';
+
+const MIN_CONTENT_LENGTH = 20;
+
+const Verification = () => {
+ const { id } = useParams();
+ const challengeId = Number(id);
+
+ const fileInput = useRef(null);
+ const [content, setContent] = useState('');
+ const [isContentValid, setIsContentValid] = useState(true);
+ const [image, setImage] = useState(null);
+ const [isUploadDisabled, setIsUploadDisabled] = useState(true);
+
+ // 폼 유효성 검사 -> 버튼 상태 관리
+ useEffect(() => {
+ if (image && content.trim() && content.length >= MIN_CONTENT_LENGTH) {
+ setIsUploadDisabled(false);
+ } else {
+ setIsUploadDisabled(true);
+ }
+ }, [image, content]);
+
+ const handleUploadImage = () => {
+ fileInput.current?.click();
+ };
+
+ const handleImage = (fileBlob: File) => {
+ setImage(fileBlob); // File 객체를 직접 설정합니다.
+
+ // 미리보기를 위해 FileReader를 사용합니다.
+ const reader = new FileReader();
+ reader.readAsDataURL(fileBlob);
+ reader.onload = () => {
+ if (reader.result && typeof reader.result === 'string') {
+ // 여기서 미리보기 이미지 URL을 설정합니다.
+ const imgElement = document.querySelector(
+ '#previewImage'
+ ) as HTMLImageElement;
+ if (imgElement) {
+ imgElement.src = reader.result;
+ }
+ }
+ };
+ };
+
+ const handleDeleteImage = (e: React.MouseEvent) => {
+ e.preventDefault();
+ setImage(null);
+ };
+
+ // 내용 유효성 검사
+ const handleContentChange = (e: React.ChangeEvent) => {
+ const newContent = e.target.value;
+ setContent(newContent);
+ // console.log(content); // test
+
+ if (newContent.trim() && newContent.length >= MIN_CONTENT_LENGTH) {
+ setIsContentValid(true);
+ } else {
+ setIsContentValid(false);
+ }
+ // console.log(isContentValid); // test
+ };
+
+ const handleSave = async () => {
+ if (image) {
+ postVerification(challengeId, content, image)
+ .then(() => {
+ alert('챌린지 인증이 등록되었습니다!');
+ handleSelectedTab(0); // 인증 기록 탭으로 이동
+ })
+ .catch((error) => {
+ // API에서 받은 오류 객체일 경우
+ if (error?.result === 'FAIL') {
+ alert(error.message || '다시 시도해주세요.');
+ } else {
+ // 예상치 못한 오류 처리
+ alert('다시 시도해주세요.');
+ }
+ });
+ }
+ };
+
+ const handleSelectedTab = (value: number) => {
+ // setActiveTab(value as 0 | 1);
+ sessionStorage.setItem('activeTab', String(value));
+ };
+
+ return (
+ <>
+
+
+ {image && (
+
+
+
+
+
+
+ )}
+
+ {
+ if (e.target.files) {
+ handleImage(e.target.files[0]);
+ }
+ }}
+ />
+ 사진 업로드
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Verification;
+
+const Wrapper = styled.form`
+ margin: 16px 0 0 0;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow-y: auto;
+ scrollbar-color: transparent transparent;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const PreviewImageContainer = styled.div`
+ position: relative;
+ margin: 16px 16px 0 16px;
+`;
+
+const PreviewImage = styled(Image)`
+ width: 100%;
+ object-fit: cover;
+ border-radius: 20px;
+ border: var(--color-grey-02) 1px solid;
+`;
+
+const DeleteImageButton = styled.button`
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ outline: none;
+ color: var(--color-grey-02);
+`;
+
+const AddImage = styled.div`
+ box-sizing: border-box;
+ border: var(--color-green-01) 1px solid;
+ border-radius: 10px;
+ background-color: var(--color-white);
+ font-size: var(--font-size-md);
+ font-weight: 700;
+ color: var(--color-green-01);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 10px 8px;
+ outline: none;
+ cursor: pointer;
+ margin: 16px 16px 32px;
+`;
diff --git a/src/pages/main/components/review/index.tsx b/src/pages/main/components/review/index.tsx
index e68d6a2..6bfb74e 100644
--- a/src/pages/main/components/review/index.tsx
+++ b/src/pages/main/components/review/index.tsx
@@ -1,9 +1,28 @@
+import { useGetRecentlyReview } from '@/apis/recently-review/getRecentlyReview.api';
import StarIcon from '@/assets/main/Star-Icon.svg';
import ProfileIcon from '@/assets/main/ZZAN-Profile.png';
-import { Box, Image, Text } from '@chakra-ui/react';
+import { Box, Image, Text, Spinner } from '@chakra-ui/react';
import styled from '@emotion/styled';
const Review = () => {
+ const { data, isLoading } = useGetRecentlyReview(0, 10);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ // 데이터가 배열인지 확인하고, 배열이 아니면 빈 배열을 기본값으로 설정
+ const reviews = Array.isArray(data?.data) ? data?.data : [];
+
return (
<>
{
최근 챌린지 리뷰
-
+ {reviews.length === 0 ? (
- 쓰레기 줍기 챌린지
+ 최근 챌린지 리뷰가 존재하지 않아요..
-
-
-
-
-
-
-
-
- 짠돌이
-
-
-
-
- 세상이 깨끗해지는 기분이에요! 포인트도 많이줘서 등급 올리기 ...
-
-
-
+ ) : (
+ reviews.map((review) => (
+
+
+ {review.challengeTitle}
+
+
+ {Array.from({ length: review.rating }, (_, index) => (
+
+ ))}
+
+
+
+
+ {review.user.nickname}
+
+
+
+
+ {review.content}
+
+
+
+ ))
+ )}
>
);
@@ -69,19 +98,29 @@ const Container = styled(Box)`
`;
const ReviewLayout = styled(Container)`
- height: 15.5625rem;
- padding: 0.9375rem 4.8125rem 0.9375rem 0.8125rem;
+ display: flex;
+ overflow-x: scroll;
+ height: 15.5rem;
+ padding: 0.9375rem 0.8125rem;
margin: 1rem;
border-radius: 1.25rem;
- background: var(--green--06, rgba(240, 244, 243, 0.75));
+ background: var(--color-green-06);
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
`;
const ChallengeLayout = styled.div`
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
width: 18rem;
height: 13.7rem;
padding: 1rem;
+ margin-right: 1rem;
border-radius: 1.25rem;
background-color: var(--color-green-01);
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
@@ -99,5 +138,13 @@ const ReviewProfileContainer = styled(Container)`
flex-direction: row;
justify-content: space-between;
width: 5.5rem;
- gap: 0.25rem;
+`;
+
+const ImageBox = styled(Image)`
+ display: flex;
+ width: 2rem;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
`;
diff --git a/src/pages/review-write/index.tsx b/src/pages/review-write/index.tsx
index a831f59..32c4628 100644
--- a/src/pages/review-write/index.tsx
+++ b/src/pages/review-write/index.tsx
@@ -2,10 +2,11 @@ import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { postReview } from '@/apis/review/review.api';
+import ChallengeTitle from '@/components/common/challenge-title';
import CTA, { CTAContainer } from '@/components/common/cta';
import Textarea from '@/components/common/form/textarea';
import { StarRating } from '@/components/common/star-rating';
-import TopBar from '@/components/features/layout/top-bar';
+import TopBar, { HEADER_HEIGHT } from '@/components/features/layout/top-bar';
import { useChallengeStore } from '@/store/useChallengeStore';
import {
formatRating,
@@ -15,6 +16,8 @@ import {
import { Box, Text } from '@chakra-ui/react';
import styled from '@emotion/styled';
+const MIN_CONTENT_LENGTH = 20;
+
const ReviewWrite = () => {
const { id } = useParams();
const challengeId = Number(id);
@@ -51,7 +54,7 @@ const ReviewWrite = () => {
selectedDifficulty &&
selectedAchievement &&
content.trim() &&
- content.length >= 20
+ content.length >= MIN_CONTENT_LENGTH
) {
setIsButtonDisabled(false);
} else {
@@ -65,7 +68,7 @@ const ReviewWrite = () => {
setContent(newContent);
// console.log(content); // test
- if (newContent.trim() && newContent.length >= 20) {
+ if (newContent.trim() && newContent.length >= MIN_CONTENT_LENGTH) {
setIsContentValid(true);
} else {
setIsContentValid(false);
@@ -100,94 +103,79 @@ const ReviewWrite = () => {
<>
-
- {categoryLabel}
- {challengeTitle}
-
-
-
- setRating(newRating)}
+ {categoryLabel && challengeTitle && (
+
+ )}
+
+
+
+ setRating(newRating)}
+ />
+
+ {rating}.0 / 5.0
+
+
+
+
+ {formatRating(rating)}
+
+
+
+
+ 체감 난이도
+
+ {difficultyList.map((d) => (
+ handleDifficultyClick(d)}
+ isSelected={selectedDifficulty === d}
+ >
+ {formatDifficulty(d)}
+
+ ))}
+
+
+
+ 성취감
+
+ {achievementList.map((a) => (
+ handleFeelingClick(a)}
+ isSelected={selectedAchievement === a}
+ >
+ {formatAchievement(a)}
+
+ ))}
+
+
+
+ 소감
+
-
- {rating}.0 / 5.0
-
-
-
-
- {formatRating(rating)}
-
-
-
-
-
- 체감 난이도
-
-
- {difficultyList.map((d) => (
- handleDifficultyClick(d)}
- isSelected={selectedDifficulty === d}
- >
- {formatDifficulty(d)}
-
- ))}
-
-
-
-
- 성취감
-
-
- {achievementList.map((a) => (
- handleFeelingClick(a)}
- isSelected={selectedAchievement === a}
- >
- {formatAchievement(a)}
-
- ))}
-
-
-
-
- 소감
-
-
-
- {content.length} / 최소 20자
-
-
-
-
- 리뷰 작성 시 주의 사항
-
-
- 해당 챌린지와 무관한 내용 또는 욕설, 도배 등의{' '}
-
- 부적절한 내용은 삭제 조치
+
+
+ 리뷰 작성 시 주의 사항
+
+ 해당 챌린지와 무관한 내용 또는 욕설, 도배 등의{' '}
+
+ 부적절한 내용은 삭제 조치
+
+ 될 수 있습니다.
- 될 수 있습니다.
-
-
+
+
+
{
<>
- {challengeGrouptitle}
+
{reviewList.length > 0 ? (
// 리뷰 있을 때
@@ -104,13 +105,6 @@ const Wrapper = styled.div`
text-align: center;
`;
-const Title = styled.div`
- font-size: var(--font-size-lg);
- font-weight: bold;
- margin: 16px 16px;
- text-align: left;
-`;
-
const ReviewList = styled.div`
position: relative;
display: flex;
diff --git a/src/pages/shorts/components/contents/index.tsx b/src/pages/shorts/components/contents/index.tsx
index 8e91354..5350e25 100644
--- a/src/pages/shorts/components/contents/index.tsx
+++ b/src/pages/shorts/components/contents/index.tsx
@@ -22,5 +22,5 @@ export default ShortsContents;
const ShortsContentsBox = styled.div`
display: flex;
flex-direction: column;
- margin: 1rem 1rem;
+ margin: 0.5rem 1rem;
`;
diff --git a/src/pages/shorts/components/image/index.tsx b/src/pages/shorts/components/image/index.tsx
index aa50834..a7e91a9 100644
--- a/src/pages/shorts/components/image/index.tsx
+++ b/src/pages/shorts/components/image/index.tsx
@@ -1,20 +1,8 @@
-import ChallengeImg from '@/assets/challenge/Challenge-Img.png';
+import ChallengeImg from '@/assets/shorts/shorts-img.png';
import { Image } from '@chakra-ui/react';
-import styled from '@emotion/styled';
const ShortsImage = () => {
- return (
- <>
-
-
-
- >
- );
+ return ;
};
export default ShortsImage;
-
-export const ShortsImageBox = styled.div`
- display: flex;
- margin: 1rem 0;
-`;
diff --git a/src/pages/shorts/components/info/category-icons.tsx b/src/pages/shorts/components/info/category-icons.tsx
index ce0225d..f9dfa18 100644
--- a/src/pages/shorts/components/info/category-icons.tsx
+++ b/src/pages/shorts/components/info/category-icons.tsx
@@ -31,7 +31,8 @@ export default CategoryIcon;
const ShortsInfoIconBox = styled.div<{ borderColor: string }>`
display: flex;
- padding: 0.75rem;
+ width: 4rem;
+ height: 3.25rem;
align-items: center;
justify-content: center;
border-radius: 100%;
diff --git a/src/pages/shorts/components/info/index.tsx b/src/pages/shorts/components/info/index.tsx
index b0247ae..6f22225 100644
--- a/src/pages/shorts/components/info/index.tsx
+++ b/src/pages/shorts/components/info/index.tsx
@@ -10,9 +10,11 @@ type Info = {
type Props = {
info?: Info;
+ onClick: (id: number) => void;
+ id: number;
};
-const ShortsInfo = ({ info }: Props) => {
+const ShortsInfo = ({ info, id, onClick }: Props) => {
return (
<>
@@ -36,7 +38,7 @@ const ShortsInfo = ({ info }: Props) => {
: '정보 없음'}
-
+ onClick(id)}>
@@ -52,6 +54,7 @@ export const ShortsInfoLayout = styled.div`
flex-direction: row;
margin-left: 1rem;
align-items: center;
+ padding: 2rem 0;
`;
export const ShortsInfoTextBox = styled.div`
@@ -69,7 +72,8 @@ export const ShortsStartBox = styled.div`
align-items: center;
text-align: center;
border-radius: 100%;
-
+ width: 3rem;
+ height: 3rem;
background-color: #5cc6ba;
- padding: 1rem;
+ /* padding: 1rem; */
`;
diff --git a/src/pages/shorts/index.tsx b/src/pages/shorts/index.tsx
index 7323566..b418c9c 100644
--- a/src/pages/shorts/index.tsx
+++ b/src/pages/shorts/index.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import ShortsContents from './components/contents';
import ShortsImage from './components/image';
@@ -13,6 +14,7 @@ import { Mousewheel, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/react';
type Short = {
+ id: number;
title: string;
content: string;
participantCount: number;
@@ -33,7 +35,8 @@ const Shorts = () => {
const [allData, setAllData] = useState([]);
const { data, isLoading } = useGetShorts(page, 20);
- console.log(data);
+ const navigate = useNavigate();
+
useEffect(() => {
if (data) {
setAllData((prevData) => [...prevData, ...data.data.data]);
@@ -48,15 +51,21 @@ const Shorts = () => {
const contentsData = shuffleArray(
allData.map((item) => ({
+ id: item.id,
title: item.title,
content: item.content,
}))
);
+ const handleNavigate = (id: number) => {
+ navigate(`/challenge/${id}`);
+ };
+
const infoData = shuffleArray(
allData.map((item) => ({
participantCount: item.participantCount,
category: item.category,
+ id: item.id,
}))
);
@@ -81,7 +90,11 @@ const Shorts = () => {
{infoData && infoData[index] && (
-
+ handleNavigate(infoData[index].id)}
+ id={infoData[index].id}
+ info={infoData[index]}
+ />
)}
))}
@@ -95,9 +108,10 @@ const Shorts = () => {
export default Shorts;
const ShortsLayout = styled.div`
- height: 100vh;
+ height: 80vh;
display: flex;
flex-direction: column;
+ gap: auto;
`;
const SwiperBox = styled(Swiper)`
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index f6121a7..5a66d33 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -61,6 +61,14 @@ const router = createBrowserRouter([
},
],
},
+ {
+ path: RouterPath.shorts,
+ element: (
+
+
+
+ ),
+ },
{
path: RouterPath.rank,
element: (
@@ -103,7 +111,7 @@ const router = createBrowserRouter([
),
},
{
- path: RouterPath.record,
+ path: `:id/${RouterPath.record}`,
element: (
@@ -120,10 +128,7 @@ const router = createBrowserRouter([
},
],
},
- {
- path: RouterPath.shorts,
- element: ,
- },
+
{
path: RouterPath.login,
element: ,