From c49599590abc880e4c19384a240f3f5839b79397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=A8=EC=83=81=EA=B7=9C?= <103256030+tkdrb12@users.noreply.github.com> Date: Thu, 3 Aug 2023 11:41:07 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 마이페이지 관련 타입추가 * feat: 마이페이지 관련 msw 데이터 및 핸들러 추가 * feat: MyPage 페이지 컴포넌트 추가 * feat: ProfileRunnerPostItem 컴포넌트 추가 * feat: ListFilter 컴포넌트 추가 * feat: 마이페이지 페이지 라우팅 추가 * refactor: list태그div에서ul로 수정 * fix : 페이지 상수에 / 추가 * refactor: runnerProfile 타입명에 Get추가 * refactor: Profile 타입을 따로 분리 * refactor: iternator 명 수정 --------- Co-authored-by: 상규 --- frontend/src/components/ListFilter.tsx | 76 +++++++ .../ProfileRunnerPostItem.tsx | 101 +++++++++ frontend/src/mocks/data/runnerProfile.json | 31 +++ frontend/src/mocks/handlers.ts | 5 + frontend/src/pages/MyPage.tsx | 200 ++++++++++++++++++ frontend/src/router.tsx | 6 + frontend/src/types/profile.ts | 21 ++ frontend/src/types/select.ts | 7 + 8 files changed, 447 insertions(+) create mode 100644 frontend/src/components/ListFilter.tsx create mode 100644 frontend/src/components/Profile/ProfileRunnerPostItem/ProfileRunnerPostItem.tsx create mode 100644 frontend/src/mocks/data/runnerProfile.json create mode 100644 frontend/src/pages/MyPage.tsx create mode 100644 frontend/src/types/profile.ts create mode 100644 frontend/src/types/select.ts diff --git a/frontend/src/components/ListFilter.tsx b/frontend/src/components/ListFilter.tsx new file mode 100644 index 000000000..672e7214e --- /dev/null +++ b/frontend/src/components/ListFilter.tsx @@ -0,0 +1,76 @@ +import { ListSelectOption } from '@/types/select'; +import React from 'react'; +import styled, { css, keyframes } from 'styled-components'; + +interface Props { + options: ListSelectOption[]; + selectOption: (value: string | number) => void; + width?: string; +} + +const ListFilter = ({ options, selectOption, width }: Props) => { + const makeHandleClickOption = (value: string | number) => () => { + if (options.filter((option) => option.value === value).length === 0) return; + + selectOption(value); + }; + + return ( + + + {options.map((option) => ( + + {option.label} + + ))} + + + ); +}; + +export default ListFilter; + +const appear = keyframes` + 0% { + transform: scaleX(0); + } + 100% { + transform: scaleX(1); + } +`; + +const underLine = css` + content: ''; + margin-top: 5px; + height: 3px; + width: calc(100% + 10px); + background-color: var(--baton-red); + animation: 0.3s ease-in ${appear}; +`; + +const S = { + FilterContainer: styled.div``, + + FilterList: styled.ul<{ $width?: string }>` + display: flex; + justify-content: space-between; + + width: ${({ $width }) => $width ?? '920px'}; + `, + + FilterItem: styled.li<{ isSelected: boolean }>` + display: flex; + flex-direction: column; + align-items: center; + + font-size: 26px; + font-weight: 700; + color: ${({ isSelected }) => (isSelected ? 'var(--baton-red)' : 'var(--gray-700)')}; + + &::after { + ${({ isSelected }) => (isSelected ? underLine : null)} + } + + cursor: pointer; + `, +}; diff --git a/frontend/src/components/Profile/ProfileRunnerPostItem/ProfileRunnerPostItem.tsx b/frontend/src/components/Profile/ProfileRunnerPostItem/ProfileRunnerPostItem.tsx new file mode 100644 index 000000000..78bdebde4 --- /dev/null +++ b/frontend/src/components/Profile/ProfileRunnerPostItem/ProfileRunnerPostItem.tsx @@ -0,0 +1,101 @@ +import Button from '@/components/common/Button'; +import Label from '@/components/common/Label'; +import { REVIEW_STATUS_LABEL_TEXT } from '@/constants'; +import { ProfileRunnerPost } from '@/types/profile'; +import React from 'react'; +import styled from 'styled-components'; + +interface Props extends ProfileRunnerPost {} + +const ProfileRunnerPostItem = ({ runnerPostId, title, deadline, reviewStatus, tags }: Props) => { + const handleClickFeedbackButton = () => { + alert('준비중인 기능입니다'); + }; + + return ( + + + {title} + + {deadline} 까지 + + + + {tags.map((tag, index) => ( + #{tag} + ))} + + + + {reviewStatus === 'DONE' ? ( + + ) : null} + + + ); +}; + +export default ProfileRunnerPostItem; + +const S = { + RunnerPostItemContainer: styled.li` + display: flex; + justify-content: space-between; + + width: 1200px; + height: 206px; + padding: 35px 40px; + + border: 0.5px solid var(--gray-500); + border-radius: 12px; + box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.2); + + cursor: pointer; + `, + + PostTitle: styled.p` + margin-bottom: 15px; + + font-size: 28px; + font-weight: 700; + `, + + DeadLineContainer: styled.div` + display: flex; + align-items: baseline; + gap: 10px; + `, + + DeadLine: styled.p` + margin-bottom: 60px; + + color: var(--gray-600); + `, + + TagContainer: styled.div` + & span { + margin-right: 10px; + + font-size: 14px; + color: var(--gray-600); + } + `, + + LeftSideContainer: styled.div``, + + RightSideContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: end; + `, +}; diff --git a/frontend/src/mocks/data/runnerProfile.json b/frontend/src/mocks/data/runnerProfile.json new file mode 100644 index 000000000..8259d0f3e --- /dev/null +++ b/frontend/src/mocks/data/runnerProfile.json @@ -0,0 +1,31 @@ +{ + "profile": { + "name": "도리토스", + "imageUrl": "profile.jpg", + "githubUrl": "github.com/shb03323", + "introduction": "안녕하세요 디투입니다." + }, + "runnerPosts": [ + { + "runnerPostId": 1, + "title": "제목", + "deadline": "마감기한", + "tags": ["java", "JAVA"], + "reviewStatus": "DONE" + }, + { + "runnerPostId": 2, + "title": "제목2", + "deadline": "마감기한2", + "tags": ["java", "자바"], + "reviewStatus": "NOT_STARTED" + }, + { + "runnerPostId": 3, + "title": "제목3", + "deadline": "마감기한3", + "tags": ["java", "자바"], + "reviewStatus": "NOT_STARTED" + } + ] +} diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 359e59f37..276920123 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -2,6 +2,7 @@ import { rest } from 'msw'; import runnerPostList from './data/runnerPostList.json'; import runnerPostDetails from './data/runnerPostDetails.json'; import supporterCardList from './data/supporterCardList.json'; +import runnerProfile from './data/runnerProfile.json'; export const handlers = [ rest.post('*/posts/runner/test', async (req, res, ctx) => { @@ -42,4 +43,8 @@ export const handlers = [ return res(ctx.delay(300), ctx.status(201), ctx.set('Content-Type', 'application/json')); }), + + rest.get('*/profile/runner', async (req, res, ctx) => { + return res(ctx.status(200), ctx.set('Content-Type', 'application/json'), ctx.json(runnerProfile)); + }), ]; diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx new file mode 100644 index 000000000..bd5dd7eca --- /dev/null +++ b/frontend/src/pages/MyPage.tsx @@ -0,0 +1,200 @@ +import ListFilter from '@/components/ListFilter'; +import ProfileRunnerPostItem from '@/components/Profile/ProfileRunnerPostItem/ProfileRunnerPostItem'; +import Avatar from '@/components/common/Avatar'; +import { BATON_BASE_URL } from '@/constants'; +import Layout from '@/layout/Layout'; +import { GetRunnerProfileResponse } from '@/types/profile'; +import { ReviewStatus } from '@/types/runnerPost'; +import { SelectOption } from '@/types/select'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +type ReviewPostOptions = SelectOption[]; + +const reviewPostOptions: ReviewPostOptions = [ + { + value: 'NOT_STARTED', + label: '대기중인 리뷰', + selected: true, + }, + { + value: 'IN_PROGRESS', + label: '진행중인 리뷰', + selected: false, + }, + { + value: 'DONE', + label: '완료된 리뷰', + selected: false, + }, +]; + +const MyPage = () => { + const [runnerProfile, setRunnerProfile] = useState(null); + + const [postOptions, setPostOptions] = useState(reviewPostOptions); + + const [isRunner, setIsRunner] = useState(true); + + const getRunnerProfile = async () => { + try { + const response = await fetch(`${BATON_BASE_URL}/profile/runner`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const supporterCardList = await response.json(); + + return supporterCardList; + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + const fetchRunnerPost = async () => { + const result = await getRunnerProfile(); + setRunnerProfile(result); + }; + + fetchRunnerPost(); + }, []); + + const selectOptions = (value: string | number) => { + const selectedOptionIndex = postOptions.findIndex((option) => option.value === value); + if (selectedOptionIndex === -1) return; + + const newOptions = postOptions.map((option, index) => { + if (index === selectedOptionIndex) return { ...option, selected: true }; + return { ...option, selected: false }; + }); + + setPostOptions(newOptions); + }; + + const filterList = () => { + const posts = runnerProfile?.runnerPosts; + if (!posts) return; + + const selectedOption = postOptions.filter((option) => option.selected)[0]; + if (!selectedOption) return; + + const filteredPosts = posts.filter((post) => post.reviewStatus === selectedOption.value); + return filteredPosts; + }; + + const handleClickSupporterButton = () => { + alert('준비중인 기능입니다'); + }; + + return ( + + + + + + {runnerProfile?.profile.name} + {runnerProfile?.profile.introduction} + + + + 러너 + + 서포터 + + + + + + + + + {filterList()?.map((item) => ( + + ))} + + + + ); +}; + +export default MyPage; + +const S = { + ProfileContainer: styled.div` + padding: 50px; + border-bottom: 1px solid var(--gray-200); + `, + + InfoContainer: styled.div` + display: flex; + align-items: center; + gap: 20px; + + height: 175px; + `, + + IntroduceContainer: styled.div` + display: flex; + flex-direction: column; + gap: 10px; + + width: 300px; + `, + + Name: styled.div` + font-size: 26px; + font-weight: 700; + `, + + Introduce: styled.div` + font-size: 20px; + + white-space: no-wrap; + overflow: hidden; + text-overflow: ellipsis; + `, + + ButtonContainer: styled.div` + display: flex; + gap: 20px; + `, + + RunnerSupporterButton: styled.button<{ isSelected: boolean }>` + display: flex; + justify-content: center; + align-items: center; + + width: 220px; + height: 38px; + border-radius: 18px; + border: 1px solid ${({ isSelected }) => (isSelected ? 'white' : 'var(--baton-red)')}; + + background-color: ${({ isSelected }) => (isSelected ? 'var(--baton-red)' : 'white')}; + + color: ${({ isSelected }) => (isSelected ? 'white' : 'var(--baton-red)')}; + `, + + PostsContainer: styled.div` + display: flex; + flex-direction: column; + align-items: center; + `, + + ListContainer: styled.ul` + display: flex; + flex-direction: column; + gap: 20px; + `, + + FilterWrapper: styled.div` + padding: 70px 20px; + `, +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index f04371e34..590bb4af0 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -6,12 +6,14 @@ import RunnerPostPage from './pages/RunnerPostDetailPage'; import RunnerPostCreatePage from './pages/RunnerPostCreatePage'; import LoginPage from './pages/LoginPage'; import CreationResultPage from './pages/CreationResultPage'; +import MyPage from './pages/MyPage'; export const ROUTER_PATH = { MAIN: '/', RUNNER_POST: '/runner-post/:runnerPostId', RUNNER_POST_CREATE: '/runner-post-create/', SUPPORTER_SELECT: '/supporter-select', + MY_PAGE: '/my-page', LOGIN: '/login', NOT_FOUND: '/*', RESULT: '/result', @@ -42,6 +44,10 @@ export const router = createBrowserRouter( path: ROUTER_PATH.RESULT, element: , }, + { + path: ROUTER_PATH.MY_PAGE, + element: , + }, ], }, ], diff --git a/frontend/src/types/profile.ts b/frontend/src/types/profile.ts new file mode 100644 index 000000000..e8df3999c --- /dev/null +++ b/frontend/src/types/profile.ts @@ -0,0 +1,21 @@ +import { ReviewStatus } from './runnerPost'; + +export interface GetRunnerProfileResponse { + profile: Profile; + runnerPosts: ProfileRunnerPost[]; +} + +export interface ProfileRunnerPost { + runnerPostId: number; + title: string; + deadline: string; + tags: string[]; + reviewStatus: ReviewStatus; +} + +export interface Profile { + name: string; + imageUrl: string; + githubUrl: string; + introduction: string; +} diff --git a/frontend/src/types/select.ts b/frontend/src/types/select.ts new file mode 100644 index 000000000..7d79be5cc --- /dev/null +++ b/frontend/src/types/select.ts @@ -0,0 +1,7 @@ +export interface SelectOption { + value: T; + label: string; + selected: boolean; +} + +export type ListSelectOption = SelectOption;