-
Notifications
You must be signed in to change notification settings - Fork 0
회원 상세 조회 페이지 구현 ( #issue feat/#340 ) #344
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
Changes from 5 commits
e8e502d
2a4925d
bc42a7d
dbf4a4a
0c2256f
be234b3
a719c80
1154cce
b9caaba
306ae0b
38fbc5a
1a018d0
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,58 @@ | ||
| // src/components/admin/userDetail/AdminUserDetail.styled.ts | ||
| import styled from 'styled-components'; | ||
| import { SpinnerContainer } from '../../user/mypage/Spinner.styled'; | ||
| import { Link } from 'react-router-dom'; | ||
|
|
||
| export const Container = styled.div` | ||
| width: 100%; | ||
| min-height: calc(100vh - 3rem); | ||
| flex: 1; | ||
| padding-top: 7rem; | ||
| `; | ||
|
|
||
| export const Spinner = styled(SpinnerContainer)``; | ||
|
|
||
| export const Wrapper = styled.div` | ||
| width: 100%; | ||
| height: 60%; | ||
| display: flex; | ||
| gap: 1rem; | ||
|
|
||
| @media ${({ theme }) => theme.mediaQuery.tablet} { | ||
| padding: 0 10px; | ||
| } | ||
| `; | ||
|
|
||
| export const UserNameArea = styled.div``; | ||
|
|
||
| export const UserName = styled.h3``; | ||
|
|
||
| export const ContentHeader = styled.div` | ||
| margin-left: 25px; | ||
| padding: 24px 0 0 24px; | ||
| `; | ||
|
|
||
| export const BackToList = styled(Link)``; | ||
|
|
||
| export const MainContent = styled.div` | ||
| height: 80%; | ||
| flex: 1; | ||
| background: ${({ theme }) => theme.color.lightgrey}; | ||
| border: 1px solid ${({ theme }) => theme.color.grey}; | ||
| border-radius: ${({ theme }) => theme.borderRadius.large}; | ||
| `; | ||
|
|
||
| export const Content = styled.div` | ||
| display: flex; | ||
| gap: 0.5rem; | ||
| padding: 24px; | ||
| `; | ||
|
|
||
| export const DetailContent = styled.div` | ||
| flex: 1 1 0; | ||
| height: 80vh; | ||
| overflow-y: auto; | ||
| border: 2px solid #f0f0f0; | ||
| border-radius: 30px; | ||
| padding: 2rem; | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import React from 'react'; | ||
| import * as S from './AdminUserDetail.styled'; | ||
| import { | ||
| InformationCircleIcon, | ||
| ClipboardDocumentListIcon, | ||
| UserGroupIcon, | ||
| } from '@heroicons/react/24/outline'; | ||
| import { ADMIN_ROUTE } from '../../../constants/routes'; | ||
| import { Outlet, useParams } from 'react-router-dom'; | ||
| import AdminTitle from '../../common/admin/title/AdminTitle'; | ||
| import useGetUserInfo from '../../../hooks/admin/useGetUserInfo'; | ||
| import Spinner from '../../user/mypage/Spinner'; | ||
| import Sidebar from '../../common/sidebar/Sidebar'; | ||
| import ScrollPreventor from '../../common/modal/ScrollPreventor'; | ||
|
|
||
| type TabKey = 'basic' | 'log' | 'inquiry' | 'joined' | 'created' | 'applied'; | ||
|
|
||
| const AdminUserDetail = () => { | ||
| const { userId } = useParams(); | ||
| const { userData, isLoading, isFetching } = useGetUserInfo(Number(userId)); | ||
|
|
||
| if (isLoading || isFetching) { | ||
| return ( | ||
| <S.Spinner> | ||
| <Spinner /> | ||
| </S.Spinner> | ||
| ); | ||
| } | ||
|
Comment on lines
+22
to
+28
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. 🛠️ Refactor suggestion 로딩 외 에러 상태 미처리
🤖 Prompt for AI Agents |
||
|
|
||
| const tabs: { | ||
| key: TabKey; | ||
| path: string; | ||
| label: string; | ||
| icon: React.ReactNode; | ||
| }[] = [ | ||
| { | ||
| key: 'basic', | ||
| label: '기본 정보', | ||
| path: `/admin/users/${userId}/${ADMIN_ROUTE.basic}`, | ||
| icon: <InformationCircleIcon width='17px' height='17px' />, | ||
| }, | ||
| { | ||
| key: 'log', | ||
| label: '활동 알림', | ||
| path: `/admin/users/${userId}/${ADMIN_ROUTE.log}`, | ||
| icon: <ClipboardDocumentListIcon width='17px' height='17px' />, | ||
| }, | ||
| { | ||
| key: 'joined', | ||
| label: '참여 프로젝트', | ||
| path: `/admin/users/${userId}/${ADMIN_ROUTE.joinedProject}`, | ||
| icon: <UserGroupIcon width='17px' height='17px' />, | ||
| }, | ||
| { | ||
| key: 'created', | ||
| label: '기획 프로젝트', | ||
| path: `/admin/users/${userId}/${ADMIN_ROUTE.createdProject}`, | ||
| icon: <UserGroupIcon width='17px' height='17px' />, | ||
| }, | ||
| { | ||
| key: 'applied', | ||
| label: '지원한 프로젝트', | ||
| path: `/admin/users/${userId}/${ADMIN_ROUTE.appliedProject}`, | ||
| icon: <ClipboardDocumentListIcon width='17px' height='17px' />, | ||
| }, | ||
| ]; | ||
|
|
||
| return ( | ||
| <ScrollPreventor> | ||
| <S.Container> | ||
| <AdminTitle title='회원 상세' /> | ||
|
|
||
| <S.Wrapper> | ||
| <S.MainContent> | ||
| <S.ContentHeader> | ||
| <S.BackToList to={`/admin/${ADMIN_ROUTE.users}`}> | ||
| 목록으로 이동 | ||
| </S.BackToList> | ||
| </S.ContentHeader> | ||
| <S.Content> | ||
| <Sidebar | ||
| menuItems={tabs} | ||
| nickname={userData?.nickname} | ||
| profileImage={userData?.profileImg} | ||
| /> | ||
| <S.DetailContent> | ||
| <Outlet | ||
| context={{ | ||
| userInfoData: userData, | ||
| }} | ||
| /> | ||
| </S.DetailContent> | ||
| </S.Content> | ||
| </S.MainContent> | ||
| </S.Wrapper> | ||
| </S.Container> | ||
| </ScrollPreventor> | ||
| ); | ||
| }; | ||
|
|
||
| export default AdminUserDetail; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,27 @@ export const Container = styled.div` | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 10px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const Wrapper = styled.div` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| position: relative; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justify-content: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const BanArea = styled.div` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| position: absolute; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| top: 0px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| right: 0px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const BanButton = styled.button` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 40px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: 1px solid ${({ theme }) => theme.color.lightgrey}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-radius: ${({ theme }) => theme.borderRadius.primary}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background-color: ${({ theme }) => theme.color.red}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: ${({ theme }) => theme.color.white}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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. 🛠️ Refactor suggestion Ban 버튼 접근성·사용성 강화 필요
export const BanButton = styled.button`
+ height: 40px;
width: 40px;
border: 1px solid ${({ theme }) => theme.color.lightgrey};
border-radius: ${({ theme }) => theme.borderRadius.primary};
background-color: ${({ theme }) => theme.color.red};
color: ${({ theme }) => theme.color.white};
+ cursor: pointer;
+
+ &:hover,
+ &:focus-visible {
+ filter: brightness(1.1);
+ outline: 2px solid ${({ theme }) => theme.color.red};
+ }
`;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const ProfileHeader = styled.div` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flex-direction: column; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,35 @@ | ||
| import React from 'react'; | ||
| import * as S from './UserCard.styled'; | ||
| import Avatar from '../../common/avatar/Avatar'; | ||
| import { type AllUser } from '../../../models/auth'; | ||
| import type { AllUser } from '../../../models/auth'; | ||
| import { formatDate } from '../../../util/formatDate'; | ||
|
|
||
| interface UserCardProps { | ||
| userData: AllUser; | ||
| onBan: (userId: number) => void; | ||
| } | ||
|
|
||
| const UserCard = ({ userData }: UserCardProps) => { | ||
| const UserCard = ({ userData, onBan }: UserCardProps) => { | ||
| return ( | ||
| <S.Container> | ||
| <S.ProfileHeader> | ||
| <Avatar image={userData.profileImg} size='46px' /> | ||
| <S.NickName>{userData.nickname}</S.NickName> | ||
| </S.ProfileHeader> | ||
| <S.Wrapper> | ||
| <S.ProfileHeader> | ||
| <Avatar image={userData.profileImg} size='46px' /> | ||
| <S.NickName>{userData.nickname}</S.NickName> | ||
| </S.ProfileHeader> | ||
| {/* {userData.userState !== '정지' && */} | ||
|
|
||
| <S.BanArea | ||
| onClick={(e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| onBan(userData.id); | ||
| }} | ||
| > | ||
| <S.BanButton>퇴출</S.BanButton> | ||
| </S.BanArea> | ||
|
Comment on lines
+22
to
+30
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. onBan 콜백이 구현되지 않았습니다 현재 🤖 Prompt for AI Agents |
||
| {/* } */} | ||
| </S.Wrapper> | ||
| <S.MainContentArea> | ||
| <S.TextLabel>이메일</S.TextLabel> | ||
| <S.TextContent>{userData.email}</S.TextContent> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,11 +24,16 @@ const Sidebar = ({ menuItems, profileImage, nickname }: SidebarProps) => { | |
| const location = useLocation(); | ||
| const isUserPage = location.pathname.includes('/user'); | ||
| const isManagePage = location.pathname.includes('/manage'); | ||
| const isAdmin = location.pathname.includes('/admin'); | ||
|
|
||
|
Comment on lines
+27
to
28
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. 🛠️ Refactor suggestion
여러 컴포넌트에서 동일한 🤖 Prompt for AI Agents |
||
| const isMyProfile = isLoggedIn && !isUserPage && !isManagePage; | ||
| const getActiveIndex = useCallback(() => { | ||
| const currentPath = location.pathname; | ||
| return menuItems.findIndex((item) => currentPath === item.path) ?? 0; | ||
| return ( | ||
| menuItems.findIndex((item) => { | ||
| return currentPath === item.path; | ||
| }) ?? 0 | ||
| ); | ||
| }, [location.pathname, menuItems]); | ||
|
|
||
| return ( | ||
|
|
@@ -53,6 +58,7 @@ const Sidebar = ({ menuItems, profileImage, nickname }: SidebarProps) => { | |
| <S.MenuItem | ||
| $isActive={getActiveIndex() === index} | ||
| $isHidden={index === 2 && isDone} | ||
| $isAdmin={isAdmin} | ||
| > | ||
| {icon && <S.IconWrapper>{icon}</S.IconWrapper>} | ||
| {icon && <S.Label>{label}</S.Label>} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ interface ContentProps { | |||||||
| export default function ContentTab({ filter, $justifyContent }: ContentProps) { | ||||||||
| const { pathname } = useLocation(); | ||||||||
| const [filterId, setFilterId] = useState<number>(); | ||||||||
| const isAdmin = pathname.includes('/admin'); | ||||||||
|
|
||||||||
|
Comment on lines
+23
to
24
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. 🛠️ Refactor suggestion
-const isAdmin = pathname.includes('/admin');
+const isAdmin = /^\/admin(\/|$)/.test(pathname); // 혹은 공통 훅으로 추출📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| function handleChangeId(id: number) { | ||||||||
| setFilterId(id); | ||||||||
|
|
@@ -59,7 +60,7 @@ export default function ContentTab({ filter, $justifyContent }: ContentProps) { | |||||||
| {pathname.includes('inquiries') ? ( | ||||||||
| <> | ||||||||
| <S.WrapperButton $height='10%'> | ||||||||
| <MovedInquiredLink /> | ||||||||
| {!isAdmin && <MovedInquiredLink />} | ||||||||
| </S.WrapperButton> | ||||||||
| <ScrollWrapper $height='10%'> | ||||||||
| <S.FilterContainer> | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,18 @@ | ||
| import { ACTIVITY_FILTER } from '../../../../constants/user/myPageFilter'; | ||
| import { useLocation } from 'react-router-dom'; | ||
| import { | ||
| ACTIVITY_FILTER, | ||
| ACTIVITY_FILTER_ADMIN, | ||
| } from '../../../../constants/user/myPageFilter'; | ||
| import ContentTab from '../ContentTab'; | ||
|
|
||
| export default function ActivityLog() { | ||
| return <ContentTab $justifyContent='space-around' filter={ACTIVITY_FILTER} />; | ||
| const { pathname } = useLocation(); | ||
| const isAdmin = pathname.includes('/admin'); | ||
|
|
||
| return ( | ||
| <ContentTab | ||
| $justifyContent='space-around' | ||
| filter={isAdmin ? ACTIVITY_FILTER_ADMIN : ACTIVITY_FILTER} | ||
| /> | ||
| ); | ||
| } |
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.
userId유효성 체크 누락으로 NaN 호출 가능useParams()가 문자열 또는undefined를 반환할 수 있는데, 곧바로Number()를 호출하면NaN이 발생합니다. 훅 내부에서 NaN 을 처리하지 않으면 불필요한 API 호출이나 런타임 오류가 발생할 수 있습니다.📝 Committable suggestion
🤖 Prompt for AI Agents