Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/components/admin/adminUserDetail/AdminUserDetail.styled.ts
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;
`;
Comment on lines +7 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

고정 높이(800px)는 반응형 이슈를 유발할 수 있습니다

Container 높이를 픽스 값으로 두면 뷰포트가 작은 노트북이나 모바일 환경에서 컨텐츠가 잘리거나 스크롤이 불필요하게 중첩될 수 있습니다.

-export const Container = styled.div`
-  width: 100%;
-  height: 800px;
+export const Container = styled.div`
+  width: 100%;
+  min-height: 100vh; /* 또는 calc(100vh - 헤더높이) */
   margin: 6rem auto 0;
   display: flex;
   flex-direction: column;
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Container = styled.div`
width: 100%;
height: 800px;
margin: 6rem auto 0;
display: flex;
flex-direction: column;
`;
export const Container = styled.div`
width: 100%;
min-height: 100vh; /* 또는 calc(100vh - 헤더높이) */
margin: 6rem auto 0;
display: flex;
flex-direction: column;
`;
🤖 Prompt for AI Agents
In src/components/admin/adminUserDetail/AdminUserDetail.styled.ts lines 7 to 13,
the Container component uses a fixed height of 800px, which can cause
responsiveness issues on smaller screens. Replace the fixed height with a
flexible value such as min-height or use relative units like vh or percentages
to allow the container to adapt to different viewport sizes and prevent content
clipping or unnecessary scrolling.


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;
`;
102 changes: 102 additions & 0 deletions src/components/admin/adminUserDetail/AdminUserDetail.tsx
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

userId 유효성 체크 누락으로 NaN 호출 가능

useParams()가 문자열 또는 undefined를 반환할 수 있는데, 곧바로 Number()를 호출하면 NaN이 발생합니다. 훅 내부에서 NaN 을 처리하지 않으면 불필요한 API 호출이나 런타임 오류가 발생할 수 있습니다.

-  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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AdminUserDetail = () => {
const { userId } = useParams();
const { userData, isLoading, isFetching } = useGetUserInfo(Number(userId));
const AdminUserDetail = () => {
const { userId } = useParams();
// guard: userId 가 없거나 숫자가 아니면 404 로 리다이렉트
const parsedId = Number(userId);
if (!userId || Number.isNaN(parsedId)) {
return <Navigate to="/404" replace />;
}
const { userData, isLoading, isFetching } = useGetUserInfo(parsedId);
🤖 Prompt for AI Agents
In src/components/admin/adminUserDetail/AdminUserDetail.tsx around lines 18 to
21, the userId from useParams() is converted to a number without validation,
which can result in NaN if userId is undefined or not a valid number. Add a
check to ensure userId is defined and a valid number before calling
useGetUserInfo. If invalid, handle the case appropriately to prevent unnecessary
API calls or runtime errors.

if (isLoading || isFetching) {
return (
<S.Spinner>
<Spinner />
</S.Spinner>
);
}
Comment on lines +22 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

로딩 외 에러 상태 미처리

isLoading, isFetching 외에 error(혹은 isError) 상태가 있을 경우 사용자에게 빈 화면이 노출됩니다. 에러 상태를 명시적으로 처리해 주세요.

🤖 Prompt for AI Agents
In src/components/admin/adminUserDetail/AdminUserDetail.tsx around lines 22 to
28, the current code handles loading and fetching states but does not handle
error states like error or isError. Update the component to explicitly check for
error conditions and render an appropriate error message or UI to inform the
user instead of showing a blank screen.


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
Expand Up @@ -27,12 +27,16 @@ const AllUserPreview = () => {
<S.Wrapper key={user.id}>
<S.UserArea>
<Avatar image={user.profileImg} size='40px' />
<S.ContentArea to={`${ADMIN_ROUTE.allUser}/${user.id}`}>
<S.ContentArea
to={`${ADMIN_ROUTE.admin}/${ADMIN_ROUTE.users}/${user.id}`}
>
<S.NickName>{user.nickname}</S.NickName>
<S.Email>{user.email}</S.Email>
</S.ContentArea>
</S.UserArea>
<S.MoveToUsersArea to={`${ADMIN_ROUTE.allUser}/${user.id}`}>
<S.MoveToUsersArea
to={`${ADMIN_ROUTE.admin}/${ADMIN_ROUTE.users}/${user.id}`}
>
<S.Text>상세 보기</S.Text>
<S.Arrow src={arrow_right} />
</S.MoveToUsersArea>
Expand Down
27 changes: 27 additions & 0 deletions src/components/admin/userCard/UserCard.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@ 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;
height: 25px;
border: 1px solid ${({ theme }) => theme.color.lightgrey};
border-radius: ${({ theme }) => theme.borderRadius.primary};
background-color: ${({ theme }) => theme.color.red};
color: ${({ theme }) => theme.color.white};
font-weight: 600;

&:hover {
background-color: #ff8789;
}
`;

export const ProfileHeader = styled.div`
display: flex;
flex-direction: column;
Expand Down
27 changes: 21 additions & 6 deletions src/components/admin/userCard/UserCard.tsx
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

onBan 콜백이 구현되지 않았습니다

현재 onBan 은 상위에서 빈 함수로 전달됩니다. 실제 정지 요청을 서버에 보내거나 모달을 띄우는 로직이 필요합니다. 미구현이면 TODO 주석이라도 남겨 명시해 주세요.

🤖 Prompt for AI Agents
In src/components/admin/userCard/UserCard.tsx around lines 22 to 30, the onBan
callback is currently a no-op passed from the parent component. Implement the
onBan function to either send a ban request to the server or open a confirmation
modal. If the implementation is pending, add a TODO comment in the code to
clearly indicate that this functionality needs to be completed.

{/* } */}
</S.Wrapper>
<S.MainContentArea>
<S.TextLabel>이메일</S.TextLabel>
<S.TextContent>{userData.email}</S.TextContent>
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/noContent/NoContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export default function NoContent({ type }: NoContentProps) {
applicants: '지원자가',
passNonPass: '합/불합격자 리스트가',
notification: '알림이',
comment: '댓글이',
inquiries: '문의글이',
comment: '댓글이',
inquiries: '문의글이',
};

return (
Expand Down
17 changes: 11 additions & 6 deletions src/components/common/sidebar/Sidebar.styled.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import styled from 'styled-components';

export const Container = styled.div`
export const Container = styled.div<{ $isAdmin: boolean }>`
display: flex;
flex-direction: column;
border: 2px solid #f0f0f0;
border-radius: ${({ theme }) => theme.borderRadius.large};
width: 22%;
min-width: 130px;
height: 80vh;
min-width: ${({ $isAdmin }) => ($isAdmin ? `200px` : `130px`)};
margin-right: 1.25rem;
padding-bottom: 1rem;
`;
Expand Down Expand Up @@ -49,16 +48,22 @@ export const MenuList = styled.div`
export const MenuItem = styled.div<{
$isActive: boolean;
$isHidden?: boolean;
$isAdmin?: boolean;
}>`
display: ${({ $isHidden }) => ($isHidden ? 'none' : 'flex')};
align-items: center;
padding: 0.625rem 1.25rem;
margin: 0.5rem 0;
background-color: ${({ $isActive }) =>
$isActive ? '#f9f9f9' : 'transparent'};
background-color: ${({ theme, $isActive, $isAdmin }) =>
$isActive
? $isAdmin
? theme.color.white
: theme.color.lightgrey
: 'transparent'};

&:hover {
background-color: #f9f9f9;
background-color: ${({ theme, $isAdmin }) =>
$isAdmin ? theme.color.white : theme.color.lightgrey};
}

svg {
Expand Down
10 changes: 8 additions & 2 deletions src/components/common/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

isAdmin 중복 로직 공통화 권장

여러 컴포넌트에서 동일한 pathname.includes('/admin') 체크가 반복되고 있습니다. 전역 util 함수나 커스텀 훅으로 추출해 중복을 제거하면 유지보수성이 높아집니다.

🤖 Prompt for AI Agents
In src/components/common/sidebar/Sidebar.tsx around lines 27 to 28, the check
for isAdmin using location.pathname.includes('/admin') is duplicated across
multiple components. Refactor by extracting this logic into a global utility
function or a custom hook that returns the isAdmin boolean. Replace the inline
check with a call to this shared function or hook to improve maintainability and
reduce code duplication.

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 ? (
Expand All @@ -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>}
Expand Down
3 changes: 2 additions & 1 deletion src/components/user/mypage/ContentTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

isAdmin 판별 로직 개선 제안

pathname.includes('/admin')/administrator 같이 의도치 않은 경로까지 관리자 페이지로 오인할 가능성이 있습니다. 정규식이나 startsWith를 사용해 세그먼트 단위로 확인하거나 공용 유틸/훅(useIsAdminPath())으로 분리해 중복을 제거하는 편이 안전합니다.

-const isAdmin = pathname.includes('/admin');
+const isAdmin = /^\/admin(\/|$)/.test(pathname); // 혹은 공통 훅으로 추출
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isAdmin = pathname.includes('/admin');
// 기존: const isAdmin = pathname.includes('/admin');
const isAdmin = /^\/admin(\/|$)/.test(pathname); // 혹은 공통 훅(useIsAdminPath)으로 추출
🤖 Prompt for AI Agents
In src/components/user/mypage/ContentTab.tsx around lines 23 to 24, the current
check using pathname.includes('/admin') can mistakenly identify paths like
'/administrator' as admin. Replace this with a more precise check using a
regular expression or pathname.startsWith('/admin') to ensure only exact admin
paths match. Alternatively, extract this logic into a shared utility or custom
hook like useIsAdminPath() to centralize and reuse the admin path detection
logic safely.

function handleChangeId(id: number) {
setFilterId(id);
Expand Down Expand Up @@ -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>
Expand Down
Loading