diff --git a/frontend/src/apis/endpoints.ts b/frontend/src/apis/endpoints.ts index 45d334ee7..db5232c15 100644 --- a/frontend/src/apis/endpoints.ts +++ b/frontend/src/apis/endpoints.ts @@ -1,7 +1,9 @@ const endPoint = { - postingReview: `${process.env.API_BASE_URL}reviews`, + postingReview: `${process.env.API_BASE_URL}/reviews`, gettingDetailedReview: (reviewId: number, memberId: number) => - `${process.env.API_BASE_URL}reviews/${reviewId}?memberId=${memberId}`, + `${process.env.API_BASE_URL}/reviews/${reviewId}?memberId=${memberId}`, + gettingReviewList: (revieweeId: number, lastReviewId: number, memberId: number) => + `${process.env.API_BASE_URL}/reviews?revieweeId=${revieweeId}&lastReviewId=${lastReviewId}&memberId=${memberId}`, gettingInfoToWriteReview: (reviewerGroupId: number) => `/reviewer-groups/${reviewerGroupId}`, gettingKeyword: `${process.env.API_BASE_URL}keywords`, }; diff --git a/frontend/src/apis/review.ts b/frontend/src/apis/review.ts index 49b873491..24e82882b 100644 --- a/frontend/src/apis/review.ts +++ b/frontend/src/apis/review.ts @@ -33,7 +33,7 @@ export const postReviewApi = async ({ reviewData }: { reviewData: ReviewData }) return data; }; -// 상세리뷰 +// 상세 리뷰 export const getDetailedReviewApi = async ({ reviewId, memberId }: { reviewId: number; memberId: number }) => { const response = await fetch(endPoint.gettingDetailedReview(reviewId, memberId), { method: 'GET', @@ -49,3 +49,31 @@ export const getDetailedReviewApi = async ({ reviewId, memberId }: { reviewId: n const data = await response.json(); return data; }; + +// 리뷰 리스트 +export const getReviewListApi = async ({ + revieweeId, + lastReviewId, + memberId, +}: { + revieweeId: number; + lastReviewId: number; + memberId: number; +}) => { + const response = await fetch( + `/api/reviews?revieweeId=${revieweeId}&lastReviewId=${lastReviewId}&memberId=${memberId}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + throw new Error('리뷰 리스트를 불러오는 데 실패했습니다.'); + } + + const data = await response.json(); + return data; +}; diff --git a/frontend/src/assets/lock.svg b/frontend/src/assets/lock.svg index 7c937ffdf..86c8bf0e4 100644 --- a/frontend/src/assets/lock.svg +++ b/frontend/src/assets/lock.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/src/assets/unLock.svg b/frontend/src/assets/unLock.svg index afc143b97..cefd79aa0 100644 --- a/frontend/src/assets/unLock.svg +++ b/frontend/src/assets/unLock.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/src/components/ReviewPreviewCard/index.tsx b/frontend/src/components/ReviewPreviewCard/index.tsx new file mode 100644 index 000000000..551e152f8 --- /dev/null +++ b/frontend/src/components/ReviewPreviewCard/index.tsx @@ -0,0 +1,53 @@ +import GithubLogo from '@/assets/githubLogo.svg'; +import Lock from '@/assets/lock.svg'; +import UnLock from '@/assets/unLock.svg'; + +import * as S from './styles'; + +interface ReviewPreviewCardProps { + reviewerGroup: { + name: string; + }; + createdAt: string; + contentPreview: string; + keywords: { id: number; content: string }[]; + isPublic: boolean; +} + +const ReviewPreviewCard = ({ + reviewerGroup, + createdAt, + contentPreview, + keywords, + isPublic, +}: ReviewPreviewCardProps) => { + return ( + + + +
+ +
+
+ {reviewerGroup.name} + {createdAt} +
+ + + + {isPublic ? '공개' : '비공개'} + + + + {contentPreview} + + {keywords.map((keyword) => ( +
{keyword.content}
+ ))} +
+
+ + ); +}; + +export default ReviewPreviewCard; diff --git a/frontend/src/components/ReviewPreviewCard/styles.ts b/frontend/src/components/ReviewPreviewCard/styles.ts new file mode 100644 index 000000000..d77740341 --- /dev/null +++ b/frontend/src/components/ReviewPreviewCard/styles.ts @@ -0,0 +1,85 @@ +import styled from '@emotion/styled'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + + width: 100%; + + border: 1px solid ${({ theme }) => theme.colors.lightGray}; + border-radius: 8px; + + &:hover { + cursor: pointer; + border: 1px solid ${({ theme }) => theme.colors.primaryHover}; + + & > div:first-of-type { + background-color: ${({ theme }) => theme.colors.primaryHover}; + } + } +`; + +export const Header = styled.div` + display: flex; + justify-content: space-between; + + height: 6rem; + padding: 1rem 3rem; + + background-color: ${({ theme }) => theme.colors.lightGray}; + border-top-left-radius: 0.8rem; + border-top-right-radius: 0.8rem; +`; + +export const HeaderContainer = styled.div` + display: flex; + gap: 1rem; + + img { + width: 4rem; + } +`; + +export const Title = styled.div` + font-size: 1.6rem; + font-weight: 700; +`; + +export const SubTitle = styled.div` + font-size: 1.2rem; +`; + +export const Visibility = styled.div` + display: flex; + gap: 0.6rem; + align-items: center; + font-size: 1.6rem; + font-weight: 700; + + img { + width: 2rem; + } +`; + +export const Main = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; + + padding: 2rem 3rem; + + font-size: 1.6rem; +`; + +export const Keyword = styled.div` + display: flex; + gap: 3rem; + align-items: center; + font-size: 1.4rem; + + div { + padding: 0.5rem 3rem; + background-color: ${({ theme }) => theme.colors.primaryHover}; + border-radius: 0.8rem; + } +`; diff --git a/frontend/src/components/common/Button/styles.ts b/frontend/src/components/common/Button/styles.ts index 83a385ec2..c9542aecb 100644 --- a/frontend/src/components/common/Button/styles.ts +++ b/frontend/src/components/common/Button/styles.ts @@ -53,7 +53,7 @@ export const Button = styled.button<{ buttonType: ButtonType }>` width: 10rem; height: 4rem; - padding: 2rem; + padding: 1rem 2rem; border: 0.1rem solid ${({ theme }) => theme.colors.primary}; border-radius: 0.8rem; diff --git a/frontend/src/components/common/DropDown/index.tsx b/frontend/src/components/common/DropDown/index.tsx new file mode 100644 index 000000000..649afbe7d --- /dev/null +++ b/frontend/src/components/common/DropDown/index.tsx @@ -0,0 +1,24 @@ +import * as S from './styles'; + +interface DropDownProps { + onChange: (value: string) => void; + options: string[]; +} + +const DropDown = ({ onChange, options }: DropDownProps) => { + const handleChange = (e: React.ChangeEvent) => { + onChange(e.target.value); + }; + + return ( + + {options.map((option) => ( + + ))} + + ); +}; + +export default DropDown; diff --git a/frontend/src/components/common/DropDown/styles.ts b/frontend/src/components/common/DropDown/styles.ts new file mode 100644 index 000000000..705a97f89 --- /dev/null +++ b/frontend/src/components/common/DropDown/styles.ts @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +export const Container = styled.select` + width: 12rem; + padding: 8px; + + font-size: 1.6rem; + + border: 1px solid ${({ theme }) => theme.colors.placeholder}; + border-radius: 0.8rem; +`; diff --git a/frontend/src/components/common/SearchInput/styles.ts b/frontend/src/components/common/SearchInput/styles.ts index 76f729a95..c2b76e3a6 100644 --- a/frontend/src/components/common/SearchInput/styles.ts +++ b/frontend/src/components/common/SearchInput/styles.ts @@ -10,5 +10,10 @@ export const Input = styled.input` padding: 1.6rem; border: 1px solid ${({ theme }) => theme.colors.black}; - border-radius: 1.5rem; + border-radius: 0.8rem; + + &::placeholder { + font-size: 1.2rem; + color: ${({ theme }) => theme.colors.placeholder}; + } `; diff --git a/frontend/src/components/common/index.tsx b/frontend/src/components/common/index.tsx index 88e05dc64..856fffe9e 100644 --- a/frontend/src/components/common/index.tsx +++ b/frontend/src/components/common/index.tsx @@ -1,3 +1,5 @@ +export { default as Button } from './Button'; +export { default as DropDown } from './DropDown'; export { default as SearchInput } from './SearchInput'; export { default as ProjectImg } from './ProjectImg'; export { default as ReviewDate } from './ReviewDate'; diff --git a/frontend/src/components/layouts/Sidebar/index.tsx b/frontend/src/components/layouts/Sidebar/index.tsx index deefbb4d1..5f1d05f47 100644 --- a/frontend/src/components/layouts/Sidebar/index.tsx +++ b/frontend/src/components/layouts/Sidebar/index.tsx @@ -9,8 +9,8 @@ import * as S from './styles'; const PATH = { myPage: '/user/mypage', reviewWriting: '/user/review-writing', - allReview: '/user/all-review', - detailedReview: '/user/detailed-review/0?memberId=1', + reviewPreviewList: '/user/review-preview-list', + detailedReview: '/user/detailed-review', reviewGroupManagement: '/user/review-group-management', }; @@ -25,7 +25,7 @@ const Sidebar = ({ isSidebarOpen, closeSidebar }: SidebarProps) => { const menuItems = [ { path: PATH.myPage, label: PAGE.myPage }, { path: PATH.reviewWriting, label: PAGE.reviewWriting }, - { path: PATH.allReview, label: PAGE.allReview }, + { path: PATH.reviewPreviewList, label: PAGE.reviewPreviewList }, { path: PATH.detailedReview, label: PAGE.detailedReview }, { path: PATH.reviewGroupManagement, label: PAGE.reviewGroupManagement }, ]; diff --git a/frontend/src/components/layouts/Topbar/components/SidebarOpenButton/styles.ts b/frontend/src/components/layouts/Topbar/components/SidebarOpenButton/styles.ts index 6d75aaf1e..91015c335 100644 --- a/frontend/src/components/layouts/Topbar/components/SidebarOpenButton/styles.ts +++ b/frontend/src/components/layouts/Topbar/components/SidebarOpenButton/styles.ts @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; export const HamburgerButton = styled.button` - width: 3.7rem; - height: 3.7rem; + width: 2.3rem; + height: 2.3rem; img { width: 100%; diff --git a/frontend/src/components/layouts/Topbar/index.tsx b/frontend/src/components/layouts/Topbar/index.tsx index 489ee0325..169f7aeeb 100644 --- a/frontend/src/components/layouts/Topbar/index.tsx +++ b/frontend/src/components/layouts/Topbar/index.tsx @@ -18,7 +18,7 @@ const Topbar = ({ openSidebar }: TopbarProps) => { - + diff --git a/frontend/src/components/layouts/Topbar/styles.ts b/frontend/src/components/layouts/Topbar/styles.ts index 69bda354f..fe15a5130 100644 --- a/frontend/src/components/layouts/Topbar/styles.ts +++ b/frontend/src/components/layouts/Topbar/styles.ts @@ -6,8 +6,10 @@ export const Layout = styled.section` box-sizing: border-box; width: 100%; - height: 8rem; - padding: 1.8rem 2.5rem; + height: 7rem; + padding: 2rem 2.5rem; + + border-bottom: 0.1rem solid ${({ theme }) => theme.colors.lightGray}; `; export const Container = styled.div` diff --git a/frontend/src/constants/page.ts b/frontend/src/constants/page.ts index aa0570757..39eda8e32 100644 --- a/frontend/src/constants/page.ts +++ b/frontend/src/constants/page.ts @@ -1,7 +1,7 @@ export const PAGE = { myPage: '마이페이지', reviewWriting: '리뷰 작성하기', - allReview: '전체 리뷰 보기', + reviewPreviewList: '전체 리뷰 보기', detailedReview: '상세 리뷰 보기', reviewGroupManagement: '리뷰 그룹 관리', }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 99c10d954..693be499b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -6,8 +6,8 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import App from '@/App'; import DetailedReviewPage from './pages/DetailedReviewPage'; +import ReviewPreviewListPage from './pages/ReviewPreviewListPage'; import ReviewWritingPage from './pages/ReviewWriting'; -import ReviewWritingCompletePage from './pages/ReviewWritingCompletePage'; import globalStyles from './styles/globalStyles'; import theme from './styles/theme'; @@ -25,11 +25,11 @@ const router = createBrowserRouter([ element: , }, { - path: 'user/review-writing-complete', - element: , + path: 'user/review-preview-list', + element: , }, { - path: 'user/detailed-review/:id', + path: 'user/detailed-review', element: , }, ], diff --git a/frontend/src/pages/DetailedReviewPage/index.tsx b/frontend/src/pages/DetailedReviewPage/index.tsx index 826b32bcf..0bd06a37c 100644 --- a/frontend/src/pages/DetailedReviewPage/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/index.tsx @@ -1,48 +1,41 @@ import { useState, useEffect } from 'react'; -import { useLocation, useParams } from 'react-router'; - -import { ReviewComment } from '@/components'; +import { useParams } from 'react-router'; import { DetailReviewData } from '@/types'; import { getDetailedReviewApi } from '../../apis/review'; -import KeywordSection from './components/KeywordSection'; import ReviewDescription from './components/ReviewDescription'; -import ReviewSection from './components/ReviewSection'; -import * as S from './styles'; +import ReviewViewSection from './components/ReviewViewSection'; + +const ANSWER = + '림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. '; + const COMMENT = 'VITE 쓰고 싶다.'; -const DetailedReviewPage = () => { - const { id: reviewId } = useParams(); - const location = useLocation(); - const searchParams = new URLSearchParams(location.search); - const memberId = searchParams.get('memberId'); +const DetailedReviewPage = ({}) => { + const { id } = useParams<{ id: string }>(); - const [detailedReview, setDetailedReview] = useState(); + const [detailReview, setDetailReview] = useState(MOCK_DATA); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - const fetch = async () => { - if (!reviewId) return; - try { - setIsLoading(true); - const result = await getDetailedReviewApi({ reviewId: Number(reviewId), memberId: Number(memberId) }); - - setDetailedReview(result); - setErrorMessage(''); - } catch (error) { - if (error instanceof Error) { - setErrorMessage(error.message); + useEffect(() => { + const fetchReview = async () => { + try { + const result = await getDetailedReviewApi({ reviewId: Number(id), memberId: 4 }); + + setDetailReview(result); + setErrorMessage(''); + } catch (error) { + setErrorMessage('리뷰를 불러오는 데 실패했습니다.'); + } finally { + setIsLoading(false); } - } finally { - setIsLoading(false); - } - }; + }; - useEffect(() => { - fetch(); - }, []); + fetchReview(); + }, [id]); if (isLoading) return
Loading...
; diff --git a/frontend/src/pages/ReviewPreviewListPage/components/SearchSection/index.tsx b/frontend/src/pages/ReviewPreviewListPage/components/SearchSection/index.tsx new file mode 100644 index 000000000..aece5b0a8 --- /dev/null +++ b/frontend/src/pages/ReviewPreviewListPage/components/SearchSection/index.tsx @@ -0,0 +1,23 @@ +import { Button, DropDown, SearchInput } from '@/components/common'; + +import * as S from './styles'; + +interface SearchSectionProps { + onChange: (value: string) => void; + options: string[]; + placeholder: string; +} + +const SearchSection = ({ onChange, options, placeholder }: SearchSectionProps) => { + return ( + + + +