-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FE] 작성한 리뷰를 확인할 수 있는 반응형 레이아웃 #1038
base: develop
Are you sure you want to change the base?
Changes from 8 commits
ea223e3
437ba4c
b50b5f1
13ca3f9
e8dcc78
a8d2246
eb106d6
4474aeb
e3a57a1
5f719b0
f2073d4
117735a
aa8344a
2b0aa9d
d3eb2d5
ff3e520
b0e6234
a15cd98
aee47a5
699dde2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// 임시 컴포넌트! 작성한 리뷰 확인 && 받은 리뷰 확인 아이템 | ||
|
||
import * as S from './styles'; | ||
|
||
interface ReviewListItemProps { | ||
handleClick: () => void; | ||
} | ||
|
||
const ReviewListItem = ({ handleClick }: ReviewListItemProps) => { | ||
return <S.ReviewListItem onClick={handleClick}>리뷰 목록 아이템입니다</S.ReviewListItem>; | ||
}; | ||
|
||
export default ReviewListItem; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
export const ReviewListItem = styled.li` | ||
display: flex; | ||
flex-direction: column; | ||
|
||
min-width: ${({ theme }) => theme.writtenReviewLayoutSize.width}; | ||
min-height: 20rem; | ||
|
||
border: 0.2rem solid ${({ theme }) => theme.colors.placeholder}; | ||
border-radius: ${({ theme }) => theme.borderRadius.basic}; | ||
|
||
${media.small} { | ||
min-width: 30rem; | ||
min-height: 18rem; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { WrittenReviewContent } from '../../layouts'; | ||
import { NoSelectedReviewGuide } from '../index'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface DetailedWrittenReviewProps { | ||
$isMobile: boolean; | ||
selectedReviewId: number | null; | ||
} | ||
|
||
// 라우팅으로 들어오는 경우 queryParam으로 reviewId를 가져올 수 있음 | ||
// -> 그렇다면 라우터에서 이 컴포넌트를 별도의 props 없이 호출 가능, selectedReviewId는 optional 처리 | ||
// but 일단은 props로 id를 무조건 받도록 구현해둔 상태 | ||
const DetailedWrittenReview = ({ $isMobile, selectedReviewId }: DetailedWrittenReviewProps) => { | ||
// 추후 이곳에서 직접 상세 리뷰 데이터 호출 | ||
|
||
// 라우팅으로 넘어온 경우 무조건 isMobile은 true | ||
return ( | ||
<S.DetailedWrittenReview $isMobile={$isMobile}> | ||
<WrittenReviewContent title="작성한 리뷰 상세보기"> | ||
<S.Outline> | ||
{selectedReviewId ? <div style={{ height: '120vh' }}>있다</div> : <NoSelectedReviewGuide />} | ||
</S.Outline> | ||
</WrittenReviewContent> | ||
</S.DetailedWrittenReview> | ||
); | ||
}; | ||
|
||
export default DetailedWrittenReview; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
interface DetailedWrittenReviewStyleProps { | ||
$isMobile: boolean; | ||
} | ||
|
||
export const DetailedWrittenReview = styled.div<DetailedWrittenReviewStyleProps>` | ||
${media.xSmall} { | ||
${({ $isMobile }) => | ||
$isMobile | ||
? ` | ||
display: block; | ||
width: 100%; | ||
` | ||
: ` | ||
display: none; | ||
`} | ||
} | ||
`; | ||
|
||
export const Outline = styled.div` | ||
display: flex; | ||
align-items: center; | ||
|
||
min-width: ${({ theme }) => theme.writtenReviewLayoutSize.width}; | ||
min-height: ${({ theme }) => theme.writtenReviewLayoutSize.height}; | ||
|
||
border: 0.2rem solid ${({ theme }) => theme.colors.lightGray}; | ||
border-radius: ${({ theme }) => theme.borderRadius.basic}; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import SlideArrowsIcon from '@/assets/slideArrows.svg'; | ||
|
||
import * as S from './styles'; | ||
|
||
const NoSelectedReviewGuide = () => { | ||
return ( | ||
<S.NoSelectedReview> | ||
<img src={SlideArrowsIcon} alt="" /> | ||
<p>확인할 리뷰를 선택해주세요!</p> | ||
</S.NoSelectedReview> | ||
); | ||
}; | ||
|
||
export default NoSelectedReviewGuide; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
export const NoSelectedReview = styled.section` | ||
display: flex; | ||
gap: 2rem; | ||
align-items: center; | ||
justify-content: center; | ||
|
||
margin: 0 auto; | ||
|
||
img { | ||
height: 3rem; | ||
|
||
${media.medium} { | ||
height: 2.8rem; | ||
margin-left: 2.5rem; | ||
} | ||
} | ||
|
||
p { | ||
font-size: ${({ theme }) => theme.fontSize.mediumSmall}; | ||
font-weight: bold; | ||
color: ${({ theme }) => theme.colors.disabled}; | ||
|
||
${media.medium} { | ||
font-size: ${({ theme }) => theme.fontSize.basic}; | ||
} | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import ReviewListItem from '@/components/ReviewListItem'; | ||
|
||
import { WrittenReviewContent } from '../../layouts'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface WrittenReviewListProps { | ||
handleClick: (reviewId: number) => void; | ||
} | ||
|
||
const WrittenReviewList = ({ handleClick }: WrittenReviewListProps) => { | ||
// 리뷰 리스트 받아오기 | ||
const reviewIdList = [5, 1, 2, 3, 4]; | ||
|
||
return ( | ||
<WrittenReviewContent title="작성한 리뷰 목록"> | ||
<S.WrittenReviewList> | ||
{/** 추후 이벤트 위임 형식으로 변경 가능 */} | ||
|
||
{/** TODO: 작성한 리뷰 없을 때의 컴포넌트 추가*/} | ||
{reviewIdList.map((reviewId) => ( | ||
<ReviewListItem key={reviewId} handleClick={() => handleClick(reviewId)} /> | ||
))} | ||
</S.WrittenReviewList> | ||
</WrittenReviewContent> | ||
); | ||
}; | ||
|
||
export default WrittenReviewList; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
|
||
export const WrittenReviewList = styled.ul` | ||
overflow-x: hidden; | ||
overflow-y: auto; | ||
display: flex; | ||
flex-direction: column; | ||
gap: 1.7rem; | ||
|
||
max-height: 68vh; | ||
|
||
${media.xSmall} { | ||
width: 100%; | ||
} | ||
|
||
& > li { | ||
margin-right: 0.5rem; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default as NoSelectedReviewGuide } from './NoSelectedReviewGuide'; | ||
export { default as DetailedWrittenReview } from './DetailedWrittenReview'; | ||
export { default as WrittenReviewList } from './WrittenReviewList'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as useCurrentMediaType } from './useCurrentMediaType'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { useState, useLayoutEffect } from 'react'; | ||
|
||
import { breakpoint } from '@/styles/theme'; | ||
import { Breakpoints } from '@/utils/media'; | ||
|
||
interface CurrentDevice { | ||
isMobile: boolean; | ||
isTablet: boolean; | ||
isDesktop: boolean; | ||
} | ||
|
||
/** | ||
현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅 | ||
*/ | ||
const useCurrentMediaType = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏻 현재 디바이스에 따라 다르게 동작해야 하는 경우 사용하기 좋겠네요 |
||
const [currentMediaType, setCurrentMediaType] = useState<Breakpoints | null>(null); | ||
const breakpointsArray = Object.entries(breakpoint); | ||
|
||
const getCurrentDeviceType = (mediaType: Breakpoints | null): CurrentDevice => ({ | ||
isMobile: mediaType === 'xSmall' || mediaType === 'xxSmall', | ||
isTablet: mediaType === 'small' || mediaType === 'medium', | ||
isDesktop: mediaType === 'large', | ||
}); | ||
|
||
useLayoutEffect(() => { | ||
const handleResize = () => { | ||
const currentWidth = window.innerWidth; | ||
const matchedBreakpoint = breakpointsArray.find(([, width]) => currentWidth <= width); | ||
|
||
setCurrentMediaType((matchedBreakpoint?.[0] as Breakpoints) ?? null); | ||
}; | ||
|
||
handleResize(); | ||
|
||
window.addEventListener('resize', handleResize); | ||
return () => window.removeEventListener('resize', handleResize); | ||
}, []); | ||
|
||
return { | ||
currentMediaType, | ||
currentDeviceType: getCurrentDeviceType(currentMediaType), | ||
}; | ||
}; | ||
|
||
export default useCurrentMediaType; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useState } from 'react'; | ||
import { useNavigate } from 'react-router-dom'; | ||
|
||
import { ErrorSuspenseContainer, AuthAndServerErrorFallback } from '@/components'; | ||
import { useSearchParamAndQuery } from '@/hooks'; | ||
|
||
import DetailedWrittenReview from './components/DetailedWrittenReview'; | ||
import WrittenReviewList from './components/WrittenReviewList'; | ||
import { useCurrentMediaType } from './hooks'; | ||
import * as S from './styles'; // TODO: 마지막에 import 경로들, 시맨틱 확인하기! | ||
|
||
// refactor(선택): 레이아웃 도입 등으로 이 페이지에서는 에러바운더리 + 탭 + 이하 페이지 Content 요소만 쓰도록 분리 | ||
|
||
const WrittenReviewConfirmPage = () => { | ||
const [selectedReviewId, setSelectedReviewId] = useState<number | null>(null); | ||
|
||
const { param: reviewRequestCode } = useSearchParamAndQuery({ | ||
paramKey: 'reviewRequestCode', | ||
}); | ||
const navigate = useNavigate(); | ||
const { currentDeviceType } = useCurrentMediaType(); | ||
|
||
const handleClick = (reviewId: number) => { | ||
if (currentDeviceType.isMobile) { | ||
navigate(`/user/written-review-confirm/${reviewRequestCode}/${reviewId}`); | ||
} else { | ||
setSelectedReviewId(reviewId); | ||
} | ||
}; | ||
|
||
return ( | ||
<ErrorSuspenseContainer fallback={AuthAndServerErrorFallback}> | ||
<S.PageContainer> | ||
<WrittenReviewList handleClick={handleClick} /> | ||
{/* TODO: 모바일에서 DetailedWrittenReview를 화살표 레이아웃으로 감싸기 */} | ||
{!currentDeviceType.isMobile && ( | ||
<DetailedWrittenReview $isMobile={currentDeviceType.isMobile} selectedReviewId={selectedReviewId} /> | ||
)} | ||
</S.PageContainer> | ||
</ErrorSuspenseContainer> | ||
); | ||
}; | ||
|
||
export default WrittenReviewConfirmPage; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { EssentialPropsWithChildren } from '@/types'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface WrittenReviewItemProps { | ||
title: string; | ||
} | ||
|
||
const WrittenReviewContent = ({ title, children }: EssentialPropsWithChildren<WrittenReviewItemProps>) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네이밍WrittenReviewContent 라는 네이밍이 목록, 상세보기 페이지안에서 하나의 리뷰에 대한 내용에 사용 되는 건가라고 헷갈렸어요. 용도를 보니, 리뷰 확인 페이지 내에서 목록,상세보기의 부모 컴포넌트로 레이아웃을 담당하는 것 같네요. 이미 WrittenReviewConfirmPage 폴더에 있으니, ContentLayout 이라고 해도 좋을 것 같네요. layouts 폴더 위치layouts 은 레이아웃을 담당하는 컴포넌트들을 모아두는 곳 같은데 왜 components 하위가 아니라 페이지 폴더 바로 밑에 있는지 알 수 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네이밍저도 하면서 이름을 뭘로 지을까 고민했었는데 역시 혼란의 여지가 있었군요 🥲 경로일단 이유는 이 세 가지였습니다.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 경로에 대해리뷰 작성 페이지 예시를 참고했군요! 올리가 구현한 경우는 기능별로 폴더를 구별하는 것이 아닌 컴포넌트,훅으로 폴더를 관리하고 있는 방식이라 layout이 컴포넌트 하위에 있는게 자연스러울 것 같아요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이해됐습니다!! 수정해서 올릴게요~~ |
||
return ( | ||
<S.WrittenReviewContent> | ||
<S.Title>{title}</S.Title> | ||
<S.Content>{children}</S.Content> | ||
</S.WrittenReviewContent> | ||
); | ||
}; | ||
|
||
export default WrittenReviewContent; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
export const WrittenReviewContent = styled.article` | ||
display: flex; | ||
flex-direction: column; | ||
height: 100%; | ||
|
||
${media.xSmall} { | ||
margin: 0 auto; | ||
} | ||
`; | ||
|
||
export const Title = styled.h2` | ||
margin-top: 4.7rem; | ||
margin-bottom: 2.4rem; | ||
font-size: 1.8rem; | ||
font-weight: bold; | ||
`; | ||
|
||
export const Content = styled.section` | ||
|
||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as WrittenReviewContent } from './WrittenReviewContent'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
export const PageContainer = styled.div` | ||
display: flex; | ||
gap: 6rem; | ||
justify-content: center; | ||
|
||
${media.medium} { | ||
gap: 4rem; | ||
} | ||
|
||
${media.small} { | ||
margin: 0 2rem; | ||
} | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
데스크탑에서 내가 쓴 상세 리뷰 url로 바로 들어오면, 상세보기만 보이겠네요.
회의때 모바일에서 내가 쓴 상세 리뷰를 따로 페이지로 관리하자고 했는데 이러면 url 에 따른 화면 관리가 까다롭겠네요. 상세 보기 페이지에서 뒤로 가기를 하면, 데스크탑에서 목록과 상세보기가 같이 보이는 화면이 나타나는게 이상하기도 하구요. (이전에는 상세보기만 보여줬다가 이제는 목록과 상세보기가 같이 보여진다?)
기존 내가 받은 리뷰 목록, 상세처럼 아예 페이지를 분리하는게 나을 것 같아요. 노트북으로 볼 때 지금 목록 크기 대비 두 개가 같이 있을 때 쓴 목록과 상세보기가 생각보다 작게 보일 수 도 있을 것 같네요.
이 부분은 프론트 전체 회의가 필요할 것 같네요