-
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] feat: 사용자 프로필 정보 및 프로필 탭 구현 #1035
Open
chysis
wants to merge
9
commits into
develop
Choose a base branch
from
fe/feat/1020-login-profile-tab
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
97f8bc0
feat: 사용자 프로필 컴포넌트 구현
chysis 3c31f0a
feat: 프로필 탭 컴포넌트 구현 및 관련 타입 작성
chysis 5c5c529
chore: 공통 theme의 zIndex 값 수정
chysis 0c1bc79
feat: 프로필 탭 상태 및 외부 요소 클릭 시 닫히게 하는 로직 구현
chysis 1bda6a3
feat: 로그인 탭에 들어가는 요소를 관리하는 훅 구현
chysis 94e2b43
chore: 커스텀 훅 적용 및 프로필 요소 드래그 불가능하도록 수정
chysis c7f9e48
feat: 프로필 정보 및 프로필 탭 반응형 구현
chysis 799d3d6
chore: 프로필 사진 src가 없는 경우 회색 배경을 보여주도록 수정
chysis 86806d1
chore: 프로필 탭의 모든 요소가 드래그 할 수 없도록 수정
chysis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import DownArrowIcon from '@/assets/downArrow.svg'; | ||
import UndraggableWrapper from '@/components/common/UndraggableWrapper'; | ||
import { SocialType } from '@/types/profile'; | ||
|
||
import ProfileTab from '../ProfileTab'; | ||
import useProfile from '../ProfileTab/hooks/useProfile'; | ||
import useProfileTabElements from '../ProfileTab/hooks/useProfileTabElements'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface ProfileInfoProps { | ||
profileImageSrc?: string; | ||
profileId: string; | ||
socialType: SocialType; | ||
} | ||
|
||
const ProfileInfo = ({ profileImageSrc, profileId, socialType }: ProfileInfoProps) => { | ||
const { isOpened, containerRef, handleContainerClick } = useProfile(); | ||
const { profileTabElements } = useProfileTabElements({ profileId, socialType }); | ||
|
||
return ( | ||
<S.ProfileSection ref={containerRef}> | ||
<UndraggableWrapper> | ||
<S.ProfileContainer onClick={handleContainerClick}> | ||
<S.ProfileImageWrapper> | ||
{profileImageSrc && <img src={profileImageSrc} alt="프로필 사진" />} | ||
</S.ProfileImageWrapper> | ||
<S.ProfileId>{profileId}</S.ProfileId> | ||
<S.ArrowIcon src={DownArrowIcon} $isOpened={isOpened} alt="" /> | ||
</S.ProfileContainer> | ||
</UndraggableWrapper> | ||
{isOpened && <ProfileTab items={profileTabElements} />} | ||
</S.ProfileSection> | ||
); | ||
}; | ||
|
||
export default ProfileInfo; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
interface DropdownStyleProps { | ||
$isOpened: boolean; | ||
} | ||
|
||
export const ProfileSection = styled.section` | ||
cursor: pointer; | ||
position: relative; | ||
width: fit-content; | ||
`; | ||
|
||
export const ProfileContainer = styled.div` | ||
display: flex; | ||
gap: 1rem; | ||
align-items: center; | ||
padding: 0 1rem; | ||
`; | ||
|
||
export const ProfileImageWrapper = styled.div` | ||
overflow: hidden; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
|
||
width: 4rem; | ||
height: 4rem; | ||
|
||
background-color: ${({ theme }) => theme.colors.gray}; | ||
border-radius: 2rem; | ||
`; | ||
|
||
export const ProfileId = styled.p` | ||
font-weight: ${({ theme }) => theme.fontWeight.semibold}; | ||
|
||
${media.small} { | ||
display: none; | ||
} | ||
`; | ||
|
||
export const ArrowIcon = styled.img<DropdownStyleProps>` | ||
transform: ${({ $isOpened }) => ($isOpened ? 'rotate(180deg)' : 'rotate(0deg)')}; | ||
width: 2rem; | ||
height: 2rem; | ||
transition: transform 0.3s ease-in-out; | ||
|
||
${media.small} { | ||
display: none; | ||
} | ||
`; |
28 changes: 28 additions & 0 deletions
28
frontend/src/components/profile/ProfileTab/hooks/useProfile.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
|
||
const useProfile = () => { | ||
const [isOpened, setIsOpened] = useState(false); | ||
const containerRef = useRef<HTMLDivElement>(null); | ||
|
||
const handleClickOutside = (event: MouseEvent) => { | ||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) { | ||
setIsOpened(false); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
document.addEventListener('mousedown', handleClickOutside); | ||
|
||
return () => { | ||
document.removeEventListener('mousedown', handleClickOutside); | ||
}; | ||
}, [containerRef]); | ||
|
||
const handleContainerClick = () => { | ||
setIsOpened((prev) => !prev); | ||
}; | ||
|
||
return { isOpened, containerRef, handleContainerClick }; | ||
}; | ||
|
||
export default useProfile; |
96 changes: 96 additions & 0 deletions
96
frontend/src/components/profile/ProfileTab/hooks/useProfileTabElements.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { useNavigate } from 'react-router'; | ||
|
||
import GitHubIcon from '@/assets/github.svg'; | ||
import LogoutIcon from '@/assets/logout.svg'; | ||
import MenuIcon from '@/assets/menu.svg'; | ||
import OpenedBookIcon from '@/assets/openedBook.svg'; | ||
import UserIcon from '@/assets/user.svg'; | ||
import { ProfileTabElement, SocialType } from '@/types/profile'; | ||
|
||
interface UseProfileTabElementsProps { | ||
profileId: string; | ||
socialType: SocialType; | ||
} | ||
|
||
const useProfileTabElements = ({ profileId, socialType }: UseProfileTabElementsProps) => { | ||
const navigate = useNavigate(); | ||
|
||
const handleReviewLinkControl = () => { | ||
// 리뷰 링크 관리 페이지로 이동 | ||
console.log('리뷰 링크 관리 클릭'); | ||
}; | ||
|
||
const handleCheckWrittenReviews = () => { | ||
// 작성한 리뷰 확인 페이지로 이동 | ||
console.log('작성한 리뷰 확인 클릭'); | ||
}; | ||
|
||
const handleLogout = () => { | ||
// 로그아웃 로직 | ||
console.log('로그아웃 클릭'); | ||
}; | ||
|
||
const profileTabElements: ProfileTabElement[] = [ | ||
{ | ||
elementType: 'readonly', | ||
isDisplayedOnlyMobile: false, | ||
content: socialType === 'github' && ( | ||
<div style={{ display: 'flex', gap: '1rem' }}> | ||
<img src={GitHubIcon} alt="소셜 아이콘" /> | ||
<span>GitHub 계정</span> | ||
</div> | ||
), | ||
}, | ||
{ | ||
elementType: 'readonly', | ||
isDisplayedOnlyMobile: true, | ||
content: ( | ||
<div style={{ display: 'flex', gap: '1rem' }}> | ||
<img src={UserIcon} alt="사람 아이콘" /> | ||
<span>{profileId}</span> | ||
</div> | ||
), | ||
}, | ||
{ | ||
elementType: 'action', | ||
isDisplayedOnlyMobile: false, | ||
content: ( | ||
<div style={{ display: 'flex', gap: '1rem' }}> | ||
<img src={MenuIcon} alt="메뉴 아이콘" /> | ||
<span>리뷰 링크 관리</span> | ||
</div> | ||
), | ||
handleClick: handleReviewLinkControl, | ||
}, | ||
{ | ||
elementType: 'action', | ||
isDisplayedOnlyMobile: false, | ||
content: ( | ||
<div style={{ display: 'flex', gap: '1rem' }}> | ||
<img src={OpenedBookIcon} alt="펼쳐진 책 아이콘" /> | ||
<span>작성한 리뷰 확인</span> | ||
</div> | ||
), | ||
handleClick: handleCheckWrittenReviews, | ||
}, | ||
{ | ||
elementType: 'divider', | ||
isDisplayedOnlyMobile: false, | ||
}, | ||
{ | ||
elementType: 'action', | ||
isDisplayedOnlyMobile: false, | ||
content: ( | ||
<div style={{ display: 'flex', gap: '1rem' }}> | ||
<img src={LogoutIcon} alt="펼쳐진 책 아이콘" /> | ||
<span>로그아웃</span> | ||
</div> | ||
), | ||
handleClick: handleLogout, | ||
}, | ||
]; | ||
|
||
return { profileTabElements }; | ||
}; | ||
|
||
export default useProfileTabElements; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import UndraggableWrapper from '@/components/common/UndraggableWrapper'; | ||
import { ProfileTabElement } from '@/types/profile'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface ProfileTabProps { | ||
items: ProfileTabElement[]; | ||
} | ||
|
||
const ProfileTab = ({ items }: ProfileTabProps) => { | ||
return ( | ||
<S.ProfileTabContainer> | ||
<UndraggableWrapper> | ||
{items.map((item, index) => { | ||
switch (item.elementType) { | ||
case 'readonly': | ||
return ( | ||
<S.ReadonlyItemWrapper | ||
key={`${item.elementType}_${index}`} | ||
$isDisplayedOnlyMobile={item.isDisplayedOnlyMobile} | ||
> | ||
{item.content} | ||
</S.ReadonlyItemWrapper> | ||
); | ||
case 'action': | ||
return ( | ||
<S.ActionItemWrapper | ||
key={`${item.elementType}_${index}`} | ||
onClick={item.handleClick} | ||
$isDisplayedOnlyMobile={item.isDisplayedOnlyMobile} | ||
> | ||
{item.content} | ||
</S.ActionItemWrapper> | ||
); | ||
case 'divider': | ||
return ( | ||
<S.Divider key={`${item.elementType}_${index}`} $isDisplayedOnlyMobile={item.isDisplayedOnlyMobile} /> | ||
); | ||
} | ||
})} | ||
</UndraggableWrapper> | ||
</S.ProfileTabContainer> | ||
); | ||
}; | ||
|
||
export default ProfileTab; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
interface ProfileTabStyleProps { | ||
$isDisplayedOnlyMobile: boolean; | ||
} | ||
|
||
export const ProfileTabContainer = styled.div` | ||
position: absolute; | ||
z-index: ${({ theme }) => theme.zIndex.profileTab}; | ||
top: 5rem; | ||
right: 0; | ||
|
||
display: flex; | ||
flex-direction: column; | ||
|
||
width: max-content; | ||
min-width: 100%; | ||
height: fit-content; | ||
padding: 1rem; | ||
|
||
background-color: ${({ theme }) => theme.colors.white}; | ||
border-radius: 0.8rem; | ||
box-shadow: | ||
0 0.5rem 0.5rem -0.3rem rgba(0, 0, 0, 0.2), | ||
0 0.8rem 1rem 0.1rem rgba(0, 0, 0, 0.14), | ||
0 0.3rem 1.4rem 0.2rem rgba(0, 0, 0, 0.12); | ||
`; | ||
|
||
export const ReadonlyItemWrapper = styled.div<ProfileTabStyleProps>` | ||
cursor: default; | ||
|
||
display: ${({ $isDisplayedOnlyMobile }) => ($isDisplayedOnlyMobile ? 'none' : 'flex')}; | ||
align-items: center; | ||
|
||
height: 3rem; | ||
padding: 1rem; | ||
|
||
${media.small} { | ||
display: ${({ $isDisplayedOnlyMobile }) => $isDisplayedOnlyMobile && 'flex'}; | ||
} | ||
`; | ||
|
||
export const ActionItemWrapper = styled.div<ProfileTabStyleProps>` | ||
cursor: pointer; | ||
|
||
display: ${({ $isDisplayedOnlyMobile }) => ($isDisplayedOnlyMobile ? 'none' : 'flex')}; | ||
align-items: center; | ||
|
||
height: 3rem; | ||
padding: 1rem; | ||
|
||
border-radius: 0.8rem; | ||
|
||
:hover { | ||
background-color: ${({ theme }) => theme.colors.lightGray}; | ||
} | ||
|
||
${media.small} { | ||
display: ${({ $isDisplayedOnlyMobile }) => $isDisplayedOnlyMobile && 'flex'}; | ||
} | ||
`; | ||
|
||
export const Divider = styled.hr<ProfileTabStyleProps>` | ||
display: ${({ $isDisplayedOnlyMobile }) => ($isDisplayedOnlyMobile ? 'none' : 'block')}; | ||
|
||
width: 100%; | ||
height: 0; | ||
margin: 0.5rem 0; | ||
padding: 0; | ||
|
||
border: 0.1rem solid ${({ theme }) => theme.colors.placeholder}; | ||
|
||
${media.small} { | ||
display: ${({ $isDisplayedOnlyMobile }) => $isDisplayedOnlyMobile && 'block'}; | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export type SocialType = 'github'; | ||
|
||
export type ProfileTabElementType = 'readonly' | 'action' | 'divider'; | ||
|
||
export interface ProfileTabElement { | ||
elementType: ProfileTabElementType; | ||
isDisplayedOnlyMobile: boolean; // true: 모바일만, false: 전체 | ||
content?: React.ReactNode; // divider 제외 지정 | ||
handleClick?: () => void; // action일 때 지정 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
프로필 탭은 모달 이외의 다른 모든 요소보다는 위에 떠야 하기 때문에 z-index 값을 조정했습니다.