-
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 all 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,84 @@ | ||
| // 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'; | ||
| import Button from '../../common/Button/Button'; | ||
|
|
||
| export const Container = styled.div` | ||
| width: 100%; | ||
| height: 800px; | ||
| margin: 6rem auto 0; | ||
| display: flex; | ||
| flex-direction: column; | ||
| `; | ||
|
|
||
| export const Spinner = styled(SpinnerContainer)``; | ||
|
|
||
| export const HeaderArea = styled.div` | ||
| display: flex; | ||
| justify-content: right; | ||
| align-items: center; | ||
| margin-bottom: 10px; | ||
| `; | ||
|
|
||
| export const ContentHeader = styled(Button)``; | ||
|
|
||
| export const BackToList = styled(Link)` | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| `; | ||
|
|
||
| export const Wrapper = styled.div` | ||
| height: 100%; | ||
| display: flex; | ||
| gap: 1rem; | ||
| @media ${({ theme }) => theme.mediaQuery.tablet} { | ||
| padding: 0 10px; | ||
| } | ||
| `; | ||
|
|
||
| export const UserNameArea = styled.div``; | ||
|
|
||
| export const UserName = styled.h3``; | ||
|
|
||
| export const MainContent = styled.div` | ||
| width: 100%; | ||
| height: 100%; | ||
| display: flex; | ||
| flex-direction: column; | ||
| background: ${({ theme }) => theme.color.lightgrey}; | ||
| border: 1px solid ${({ theme }) => theme.color.grey}; | ||
| border-radius: ${({ theme }) => theme.borderRadius.large}; | ||
| `; | ||
|
|
||
| export const Content = styled.div` | ||
| height: 100%; | ||
| display: flex; | ||
| gap: 0.5rem; | ||
| padding: 24px; | ||
| `; | ||
|
|
||
| export const DetailContent = styled.div` | ||
| height: 100%; | ||
| width: 100%; | ||
| overflow-y: scroll; | ||
| &::-webkit-scrollbar { | ||
| width: 8px; | ||
| position: relative; | ||
| left: 0px; | ||
| } | ||
| &::-webkit-scrollbar-thumb { | ||
| background-color: rgba(0, 0, 0, 0.2); | ||
| border-radius: 4px; | ||
| } | ||
| &::-webkit-scrollbar-track { | ||
| background: transparent; | ||
| } | ||
| border: 2px solid #f0f0f0; | ||
| border-radius: 30px; | ||
| padding: 2rem; | ||
| `; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+21
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 { userData, isLoading, isFetching } = useGetUserInfo(Number(userId));
+ // guard: userId 가 없거나 숫자가 아니면 404 로 리다이렉트
+ const parsedId = Number(userId);
+ if (!userId || Number.isNaN(parsedId)) {
+ return <Navigate to="/404" replace />;
+ }
+ const { userData, isLoading, isFetching } = useGetUserInfo(parsedId);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||
| <S.HeaderArea> | ||||||||||||||||||||||||||||
| <AdminTitle title='회원 상세' /> | ||||||||||||||||||||||||||||
| <S.ContentHeader radius='primary' schema='primary' size='primary'> | ||||||||||||||||||||||||||||
| <S.BackToList to={`/admin/${ADMIN_ROUTE.users}`}> | ||||||||||||||||||||||||||||
| 목록으로 이동 | ||||||||||||||||||||||||||||
| </S.BackToList> | ||||||||||||||||||||||||||||
| </S.ContentHeader> | ||||||||||||||||||||||||||||
| </S.HeaderArea> | ||||||||||||||||||||||||||||
| <S.Wrapper> | ||||||||||||||||||||||||||||
| <S.MainContent> | ||||||||||||||||||||||||||||
| <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 |
|---|---|---|
| @@ -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,15 +24,20 @@ 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 ( | ||
| <S.Container> | ||
| <S.Container $isAdmin={isAdmin}> | ||
| <S.AvatarContainer> | ||
| <S.AvatarWrapper> | ||
| {profileImage === MainLogo ? ( | ||
|
|
@@ -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> | ||||||||
|
|
||||||||
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.
🛠️ Refactor suggestion
고정 높이(800px)는 반응형 이슈를 유발할 수 있습니다
Container 높이를 픽스 값으로 두면 뷰포트가 작은 노트북이나 모바일 환경에서 컨텐츠가 잘리거나 스크롤이 불필요하게 중첩될 수 있습니다.
📝 Committable suggestion
🤖 Prompt for AI Agents