From e8e502d518e4df54c5cef27ab249619802fadca2 Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:42:46 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=B3=B4=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=8D=B8=EC=97=90=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/userInfo.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/models/userInfo.ts b/src/models/userInfo.ts index 8567722a..9a0f0f05 100644 --- a/src/models/userInfo.ts +++ b/src/models/userInfo.ts @@ -16,6 +16,7 @@ export interface UserInfo { profileImg?: string; beginner: boolean; github?: string; + warning: number; career?: Career[]; positions: Omit[]; skills: Omit[]; @@ -23,6 +24,12 @@ export interface UserInfo { averageScores: number[]; } +export interface UserInfoAll extends UserInfo { + email: string; + warning: number; + createdAt: string; +} + export interface ApiUserInfo extends ApiCommonType { data: UserInfo | null; } From 2a4925d593b497853a15ed08552e8cd7e8e42444 Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:05:40 +0900 Subject: [PATCH 02/11] =?UTF-8?q?style=20:=20"=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C"=20=EC=8B=9C=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=8A=A4=ED=94=BC=EB=84=88=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/admin/adminUser/AdminUser.styled.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/admin/adminUser/AdminUser.styled.ts b/src/pages/admin/adminUser/AdminUser.styled.ts index 74b4443f..808e2ffc 100644 --- a/src/pages/admin/adminUser/AdminUser.styled.ts +++ b/src/pages/admin/adminUser/AdminUser.styled.ts @@ -1,9 +1,12 @@ import styled from 'styled-components'; +import { SpinnerContainer } from '../../../components/user/mypage/Spinner.styled'; export const Container = styled.div``; +export const Spinner = styled(SpinnerContainer)``; + export const SearchBar = styled.div` - margin-top: 20px; + margin-top: 120px; `; export const ScrollArea = styled.div` From bc42a7de08787f03db531afa45d0605ee358d0db Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Sun, 15 Jun 2025 14:01:36 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat=20:=20"=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C"=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adminUserDetail/AdminUserDetail.styled.ts | 58 ++++++++++ .../admin/adminUserDetail/AdminUserDetail.tsx | 101 ++++++++++++++++++ src/components/common/noContent/NoContent.tsx | 4 +- .../common/sidebar/Sidebar.styled.ts | 12 ++- src/components/common/sidebar/Sidebar.tsx | 8 +- src/components/user/mypage/ContentTab.tsx | 3 +- .../user/mypage/activityLog/ActivityLog.tsx | 16 ++- .../myProfile/profile/Profile.styled.ts | 6 +- .../user/mypage/myProfile/profile/Profile.tsx | 42 +++++++- src/constants/admin/mainItems.ts | 4 +- src/constants/routes.ts | 3 + src/constants/user/myPageFilter.ts | 5 + src/hooks/admin/useGetUserInfo.ts | 20 ++++ src/routes/AdminRoutes.tsx | 47 ++++++++ 14 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 src/components/admin/adminUserDetail/AdminUserDetail.styled.ts create mode 100644 src/components/admin/adminUserDetail/AdminUserDetail.tsx create mode 100644 src/hooks/admin/useGetUserInfo.ts diff --git a/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts b/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts new file mode 100644 index 00000000..c5f2b2f0 --- /dev/null +++ b/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts @@ -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; +`; diff --git a/src/components/admin/adminUserDetail/AdminUserDetail.tsx b/src/components/admin/adminUserDetail/AdminUserDetail.tsx new file mode 100644 index 00000000..7c2f1b07 --- /dev/null +++ b/src/components/admin/adminUserDetail/AdminUserDetail.tsx @@ -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 ( + + + + ); + } + + const tabs: { + key: TabKey; + path: string; + label: string; + icon: React.ReactNode; + }[] = [ + { + key: 'basic', + label: '기본 정보', + path: `/admin/users/${userId}/${ADMIN_ROUTE.basic}`, + icon: , + }, + { + key: 'log', + label: '활동 알림', + path: `/admin/users/${userId}/${ADMIN_ROUTE.log}`, + icon: , + }, + { + key: 'joined', + label: '참여 프로젝트', + path: `/admin/users/${userId}/${ADMIN_ROUTE.joinedProject}`, + icon: , + }, + { + key: 'created', + label: '기획 프로젝트', + path: `/admin/users/${userId}/${ADMIN_ROUTE.createdProject}`, + icon: , + }, + { + key: 'applied', + label: '지원한 프로젝트', + path: `/admin/users/${userId}/${ADMIN_ROUTE.appliedProject}`, + icon: , + }, + ]; + + return ( + + + + + + + + + 목록으로 이동 + + + + + + + + + + + + + ); +}; + +export default AdminUserDetail; diff --git a/src/components/common/noContent/NoContent.tsx b/src/components/common/noContent/NoContent.tsx index c1245928..1d360c03 100644 --- a/src/components/common/noContent/NoContent.tsx +++ b/src/components/common/noContent/NoContent.tsx @@ -17,8 +17,8 @@ export default function NoContent({ type }: NoContentProps) { applicants: '지원자가', passNonPass: '합/불합격자 리스트가', notification: '알림이', - comment: '내 댓글이', - inquiries: '내 문의글이', + comment: '댓글이', + inquiries: '문의글이', }; return ( diff --git a/src/components/common/sidebar/Sidebar.styled.ts b/src/components/common/sidebar/Sidebar.styled.ts index fe188e68..023c6a1c 100644 --- a/src/components/common/sidebar/Sidebar.styled.ts +++ b/src/components/common/sidebar/Sidebar.styled.ts @@ -49,16 +49,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 { diff --git a/src/components/common/sidebar/Sidebar.tsx b/src/components/common/sidebar/Sidebar.tsx index 6a42d0e9..cd20658d 100644 --- a/src/components/common/sidebar/Sidebar.tsx +++ b/src/components/common/sidebar/Sidebar.tsx @@ -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'); 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) => { {icon && {icon}} {icon && {label}} diff --git a/src/components/user/mypage/ContentTab.tsx b/src/components/user/mypage/ContentTab.tsx index 51d99bf3..2c2329f6 100644 --- a/src/components/user/mypage/ContentTab.tsx +++ b/src/components/user/mypage/ContentTab.tsx @@ -20,6 +20,7 @@ interface ContentProps { export default function ContentTab({ filter, $justifyContent }: ContentProps) { const { pathname } = useLocation(); const [filterId, setFilterId] = useState(); + const isAdmin = pathname.includes('/admin'); function handleChangeId(id: number) { setFilterId(id); @@ -59,7 +60,7 @@ export default function ContentTab({ filter, $justifyContent }: ContentProps) { {pathname.includes('inquiries') ? ( <> - + {!isAdmin && } diff --git a/src/components/user/mypage/activityLog/ActivityLog.tsx b/src/components/user/mypage/activityLog/ActivityLog.tsx index 7a80902e..62bd6087 100644 --- a/src/components/user/mypage/activityLog/ActivityLog.tsx +++ b/src/components/user/mypage/activityLog/ActivityLog.tsx @@ -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 ; + const { pathname } = useLocation(); + const isAdmin = pathname.includes('/admin'); + + return ( + + ); } diff --git a/src/components/user/mypage/myProfile/profile/Profile.styled.ts b/src/components/user/mypage/myProfile/profile/Profile.styled.ts index 17e46c7d..f76afa42 100644 --- a/src/components/user/mypage/myProfile/profile/Profile.styled.ts +++ b/src/components/user/mypage/myProfile/profile/Profile.styled.ts @@ -132,8 +132,10 @@ export const LabelBox = styled.div` `; export const ChartBox = styled.div` - width: 250px; - height: 250px; + width: 100%; + max-width: 250px; + aspect-ratio: 1 / 1; + margin: 0 auto; `; export const ExplainBox = styled.div` diff --git a/src/components/user/mypage/myProfile/profile/Profile.tsx b/src/components/user/mypage/myProfile/profile/Profile.tsx index dbe1c1f2..bc6ae0d5 100644 --- a/src/components/user/mypage/myProfile/profile/Profile.tsx +++ b/src/components/user/mypage/myProfile/profile/Profile.tsx @@ -4,20 +4,25 @@ import { Link, useLocation, useOutletContext } from 'react-router-dom'; import { Radar } from 'react-chartjs-2'; import { useEffect } from 'react'; import MyProfileWrapper from '../MyProfileWrapper'; -import type { UserInfo } from '../../../../../models/userInfo'; import { PROFILE_DEFAULT_MESSAGE } from '../../../../../constants/user/myPageProfile'; import { ROUTES } from '../../../../../constants/routes'; import 'chart.js/auto'; import { chartOptions } from '../../../../../constants/evaluationChartData'; +import { formatDate } from '../../../../../util/formatDate'; +import { UserInfoAll } from '../../../../../models/userInfo'; export default function Profile() { const { userInfoData, scrollRef, - }: { userInfoData: UserInfo; scrollRef: React.RefObject } = - useOutletContext(); + }: { + userInfoData: UserInfoAll; + scrollRef?: React.RefObject; + } = useOutletContext(); + const location = useLocation(); const myPage = location.pathname.includes('mypage') ? true : false; + const admin = location.pathname.includes('admin') ? true : false; const chartData = { labels: ['책임감', '기획력', '협업능력', '성실도', '문제해결', '기술력'], @@ -31,8 +36,10 @@ export default function Profile() { }; useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; + if (scrollRef) { + if (scrollRef.current) { + scrollRef.current.scrollTop = 0; + } } }, [scrollRef]); @@ -49,6 +56,31 @@ export default function Profile() { )} + + {admin && ( + <> + + + + {userInfoData.email} + + + + + + {userInfoData.warning}번 + + + + + + + {formatDate(userInfoData.createdAt)} + + + + + )} diff --git a/src/constants/admin/mainItems.ts b/src/constants/admin/mainItems.ts index 74c17c8d..85a40fa3 100644 --- a/src/constants/admin/mainItems.ts +++ b/src/constants/admin/mainItems.ts @@ -15,8 +15,8 @@ export interface CardItem { export const cardList: CardItem[] = [ { key: 'allUsers', - title: '전체 회원 조회', - link: `${ADMIN_ROUTE.allUser}`, + title: '회원 조회', + link: `${ADMIN_ROUTE.users}`, Component: AllUserPreview, }, { diff --git a/src/constants/routes.ts b/src/constants/routes.ts index d93d7150..ddadceb3 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -50,4 +50,7 @@ export const ADMIN_ROUTE = { joinedProject: 'joined-project', createdProject: 'created-project', appliedProject: 'apply-project', + comments: 'comments', + checkingApplicant: 'checked-applicants', + applyingProject: 'applied-projects', }; diff --git a/src/constants/user/myPageFilter.ts b/src/constants/user/myPageFilter.ts index eeab4fb6..be89a4ba 100644 --- a/src/constants/user/myPageFilter.ts +++ b/src/constants/user/myPageFilter.ts @@ -26,3 +26,8 @@ export const ACTIVITY_FILTER = [ { title: '내 댓글', url: ROUTES.comments, id: 0 }, { title: '내 문의글', url: ROUTES.activityInquiries, id: 1 }, ] as const; + +export const ACTIVITY_FILTER_ADMIN = [ + { title: '댓글', url: ROUTES.comments, id: 0 }, + { title: '문의글', url: ROUTES.activityInquiries, id: 1 }, +] as const; diff --git a/src/hooks/admin/useGetUserInfo.ts b/src/hooks/admin/useGetUserInfo.ts new file mode 100644 index 00000000..a1fbc129 --- /dev/null +++ b/src/hooks/admin/useGetUserInfo.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; +import useAuthStore from '../../store/authStore'; +import { ApiUserInfo } from '../../models/userInfo'; +import { getUserInfo } from '../../api/userpage.api'; +import { userInfoKey } from '../queries/keys'; + +const useGetUserInfo = (id: number) => { + const isLoggedIn = useAuthStore.getState().isLoggedIn; + + const { data, isLoading, isFetching } = useQuery({ + queryKey: [userInfoKey.userProfile, id], + queryFn: () => getUserInfo(id), + staleTime: 1 * 60 * 1000, + enabled: isLoggedIn, + }); + + return { userData: data?.data, isLoading, isFetching }; +}; + +export default useGetUserInfo; diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index f60a7d1c..b30c4cc5 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -61,6 +61,25 @@ const InquiryAnswerWrite = lazy( ) ); const Manage = lazy(() => import('../pages/admin/adminManage/AdminManage')); +const ActivityLogComments = lazy( + () => + import( + '../components/user/mypage/activityLog/commentsActivity/CommentsActivity' + ) +); +const ActivityLogInquiries = lazy( + () => import('../components/user/mypage/activityLog/inquiries/Inquiries') +); + +const NotificationsAppliedProjects = lazy( + () => + import( + '../components/user/mypage/notifications/appliedProjects/AppliedProjects' + ) +); +const NotificationsAll = lazy( + () => import('../components/user/mypage/notifications/all/All') +); export const AdminRoutes = () => { const routeList = [ @@ -136,10 +155,38 @@ export const AdminRoutes = () => { { path: `${ADMIN_ROUTE.log}`, element: , + children: [ + { + index: true, + element: , + }, + { + path: `${ADMIN_ROUTE.comments}`, + element: , + }, + { + path: `${ADMIN_ROUTE.inquiries}`, + element: , + }, + ], }, { path: `${ADMIN_ROUTE.appliedProject}`, element: , + children: [ + { + path: `${ADMIN_ROUTE.checkingApplicant}`, + element: , + }, + { + path: `${ADMIN_ROUTE.comments}`, + element: , + }, + { + path: `${ADMIN_ROUTE.applyingProject}`, + element: , + }, + ], }, { path: `${ADMIN_ROUTE.joinedProject}`, From dbf4a4aae144808adf7f0637ea766c97c4b92634 Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Sun, 15 Jun 2025 14:02:21 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat=20:=20"=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C"=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B0=95=ED=87=B4=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../allUserPreview/AllUserPreview.tsx | 4 +-- .../admin/userCard/UserCard.styled.ts | 21 +++++++++++++++ src/components/admin/userCard/UserCard.tsx | 27 ++++++++++++++----- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx b/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx index 2af5e580..a7d76306 100644 --- a/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx +++ b/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx @@ -27,12 +27,12 @@ const AllUserPreview = () => { - + {user.nickname} {user.email} - + 상세 보기 diff --git a/src/components/admin/userCard/UserCard.styled.ts b/src/components/admin/userCard/UserCard.styled.ts index d70929e1..18bd5f1c 100644 --- a/src/components/admin/userCard/UserCard.styled.ts +++ b/src/components/admin/userCard/UserCard.styled.ts @@ -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}; +`; + export const ProfileHeader = styled.div` display: flex; flex-direction: column; diff --git a/src/components/admin/userCard/UserCard.tsx b/src/components/admin/userCard/UserCard.tsx index d51db4c2..430814b2 100644 --- a/src/components/admin/userCard/UserCard.tsx +++ b/src/components/admin/userCard/UserCard.tsx @@ -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 ( - - - {userData.nickname} - + + + + {userData.nickname} + + {/* {userData.userState !== '정지' && */} + + { + e.preventDefault(); + e.stopPropagation(); + onBan(userData.id); + }} + > + 퇴출 + + {/* } */} + 이메일 {userData.email} From 0c2256fa8888d4acc60f83779f2b057a270e617d Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Sun, 15 Jun 2025 14:02:42 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat=20:=20"=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C"=20API=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/auth.ts | 8 +++++++- src/pages/admin/adminUser/AdminUser.tsx | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/models/auth.ts b/src/models/auth.ts index aa4e95d6..dc22a4b7 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -62,7 +62,13 @@ export interface AllUser extends AllUserPreview { position: PositionTag[]; } +export interface AllUserInfo extends AllUser { + email: string; + warning: number; + createdAt: string; +} + export interface AllUserList { users: AllUser[]; - totalPages: number; + totalPage: number; } diff --git a/src/pages/admin/adminUser/AdminUser.tsx b/src/pages/admin/adminUser/AdminUser.tsx index cac91bb4..32a9948e 100644 --- a/src/pages/admin/adminUser/AdminUser.tsx +++ b/src/pages/admin/adminUser/AdminUser.tsx @@ -1,7 +1,6 @@ import * as S from './AdminUser.styled'; import AdminTitle from '../../../components/common/admin/title/AdminTitle'; import { useGetAllUsers } from '../../../hooks/admin/useGetAllUsers'; -import LoadingSpinner from '../../../components/common/loadingSpinner/LoadingSpinner'; import UserCard from '../../../components/admin/userCard/UserCard'; import ScrollPreventor from '../../../components/common/modal/ScrollPreventor'; import SearchBar from '../../../components/common/admin/searchBar/SearchBar'; @@ -10,6 +9,7 @@ import useSearchBar from '../../../hooks/admin/useSearchBar'; import { ADMIN_MODAL_MESSAGE } from '../../../constants/admin/adminModal'; import { Link } from 'react-router-dom'; import { ADMIN_ROUTE } from '../../../constants/routes'; +import Spinner from '../../../components/user/mypage/Spinner'; const AdminUser = () => { const { searchUnit, value, handleGetKeyword, handleChangePagination } = @@ -17,13 +17,19 @@ const AdminUser = () => { const { allUserData, isLoading, isFetching } = useGetAllUsers(searchUnit); if (isLoading || isFetching) { - return ; + return ( + + + + ); } if (!allUserData || allUserData.users.length === 0) { return {ADMIN_MODAL_MESSAGE.NO_RESULT}; } + const onBan = (userId: number) => {}; + return ( <> @@ -32,7 +38,7 @@ const AdminUser = () => { @@ -43,13 +49,12 @@ const AdminUser = () => { - + ))} From be234b3e16189dba0a13f519976cbf0da08caa7e Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:37:01 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat=20:=20Sidebar=20=EA=B0=80=EB=A1=9C?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=EB=A5=BC=20admin=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=99=80=20user=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8B=A4=EB=A5=B4=EA=B2=8C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/sidebar/Sidebar.styled.ts | 5 ++--- src/components/common/sidebar/Sidebar.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/common/sidebar/Sidebar.styled.ts b/src/components/common/sidebar/Sidebar.styled.ts index 023c6a1c..544831ef 100644 --- a/src/components/common/sidebar/Sidebar.styled.ts +++ b/src/components/common/sidebar/Sidebar.styled.ts @@ -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; `; diff --git a/src/components/common/sidebar/Sidebar.tsx b/src/components/common/sidebar/Sidebar.tsx index cd20658d..754607a8 100644 --- a/src/components/common/sidebar/Sidebar.tsx +++ b/src/components/common/sidebar/Sidebar.tsx @@ -37,7 +37,7 @@ const Sidebar = ({ menuItems, profileImage, nickname }: SidebarProps) => { }, [location.pathname, menuItems]); return ( - + {profileImage === MainLogo ? ( From a719c80dc16c023ad206a8d862866c6aa261eecd Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:37:28 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat=20:=20"=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C"=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20"=ED=9A=8C=EC=9B=90=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C"=20=EB=AA=A9=EB=A1=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adminUserDetail/AdminUserDetail.styled.ts | 60 +++++++++++++------ .../admin/adminUserDetail/AdminUserDetail.tsx | 15 ++--- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts b/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts index c5f2b2f0..fed21314 100644 --- a/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts +++ b/src/components/admin/adminUserDetail/AdminUserDetail.styled.ts @@ -2,19 +2,35 @@ 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%; - min-height: calc(100vh - 3rem); - flex: 1; - padding-top: 7rem; + 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` - width: 100%; - height: 60%; + height: 100%; display: flex; gap: 1rem; @@ -27,31 +43,41 @@ 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; + 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` - flex: 1 1 0; - height: 80vh; - overflow-y: auto; + 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; diff --git a/src/components/admin/adminUserDetail/AdminUserDetail.tsx b/src/components/admin/adminUserDetail/AdminUserDetail.tsx index 7c2f1b07..743aad64 100644 --- a/src/components/admin/adminUserDetail/AdminUserDetail.tsx +++ b/src/components/admin/adminUserDetail/AdminUserDetail.tsx @@ -68,15 +68,16 @@ const AdminUserDetail = () => { return ( - - + + + + + 목록으로 이동 + + + - - - 목록으로 이동 - - Date: Sun, 15 Jun 2025 20:41:32 +0900 Subject: [PATCH 08/11] =?UTF-8?q?review=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../previewComponent/allUserPreview/AllUserPreview.tsx | 8 ++++++-- src/components/admin/userCard/UserCard.styled.ts | 6 ++++++ src/pages/admin/adminUser/AdminUser.tsx | 4 +++- src/routes/AdminRoutes.tsx | 4 ++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx b/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx index a7d76306..dd1deb6c 100644 --- a/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx +++ b/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx @@ -27,12 +27,16 @@ const AllUserPreview = () => { - + {user.nickname} {user.email} - + 상세 보기 diff --git a/src/components/admin/userCard/UserCard.styled.ts b/src/components/admin/userCard/UserCard.styled.ts index 18bd5f1c..13f247ae 100644 --- a/src/components/admin/userCard/UserCard.styled.ts +++ b/src/components/admin/userCard/UserCard.styled.ts @@ -25,10 +25,16 @@ export const BanArea = styled.div` 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` diff --git a/src/pages/admin/adminUser/AdminUser.tsx b/src/pages/admin/adminUser/AdminUser.tsx index 32a9948e..a1243f5d 100644 --- a/src/pages/admin/adminUser/AdminUser.tsx +++ b/src/pages/admin/adminUser/AdminUser.tsx @@ -28,7 +28,9 @@ const AdminUser = () => { return {ADMIN_MODAL_MESSAGE.NO_RESULT}; } - const onBan = (userId: number) => {}; + const onBan = (userId: number) => { + // TODO : 버튼을 누르면 해당 유저 강퇴 조치 API 전송. + }; return ( <> diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index b30c4cc5..91572af7 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -174,6 +174,10 @@ export const AdminRoutes = () => { path: `${ADMIN_ROUTE.appliedProject}`, element: , children: [ + { + index: true, + element: , + }, { path: `${ADMIN_ROUTE.checkingApplicant}`, element: , From b9caaba89138d8f7d047f8687e9ab3a85668dd52 Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:47:51 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat=20:=20"=EC=B0=B8=EC=97=AC=ED=95=9C/?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C"=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=B9=B4=EB=93=9C=EC=97=90=EC=84=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=90=90=EC=9D=84=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=83=88=EC=B0=BD=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/userPage/userProjectList/UserProjectList.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/user/userPage/userProjectList/UserProjectList.tsx b/src/components/user/userPage/userProjectList/UserProjectList.tsx index 829b60b6..86220b42 100644 --- a/src/components/user/userPage/userProjectList/UserProjectList.tsx +++ b/src/components/user/userPage/userProjectList/UserProjectList.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import * as S from '../../mypage/joinedProject/MyJoinProjects.styled'; import { ROUTES } from '../../../../constants/routes'; import NoContent from '../../../common/noContent/NoContent'; @@ -8,6 +8,8 @@ import Project from '../../mypage/joinedProject/Project'; import { useGetUserProjectList } from '../../../../hooks/user/useGetUserProjectList'; export default function UserProjects() { + const { pathname } = useLocation(); + const isAdmin = pathname.includes('/admin'); const { userProjectData, isLoading, title } = useGetUserProjectList(); if (isLoading) { @@ -26,6 +28,8 @@ export default function UserProjects() { From 306ae0bcbf14e027eedd39bed385b3ee06869d77 Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:51:48 +0900 Subject: [PATCH 10/11] chore: conflict resolved --- src/api/admin/customerService/inquiry.api.ts | 61 +++++ src/api/admin/customerService/notice.api.ts | 30 +++ src/api/admin/tag.api.ts | 69 +++++ .../admin/adminTags/AdminTagCRUD.styled.ts | 84 ++++++ .../admin/adminTags/AdminTagCRUD.tsx | 254 ++++++++++++++++++ .../admin/adminTags/AdminTagsBasic.styled.ts | 25 ++ .../admin/adminTags/AdminTagsBasic.tsx | 83 ++++++ .../positions/AdminPositionItems.styled.ts | 8 + .../positions/AdminPositionItems.tsx | 26 ++ .../adminTags/positions/AdminPositionTags.tsx | 5 + .../skills/AdminSkillTagItems.styled.ts | 3 + .../adminTags/skills/AdminSkillTagItems.tsx | 30 +++ .../admin/adminTags/skills/AdminSkillTags.tsx | 5 + .../sidebar/sidebarList/AdminSidebarList.tsx | 3 +- .../positionButton/PositionButton.styled.ts | 2 - .../common/positionButton/PositionButton.tsx | 25 +- .../common/skillTagBox/SkillTagBox.tsx | 13 +- .../common/skillTagBox/skillTag/SkillTag.tsx | 1 + .../skillTag/skillTagImg/SkillTagImg.tsx | 8 +- .../projectCardLists/cardList/CardList.tsx | 1 - .../filteringContents/FilteringContents.tsx | 6 +- .../myProfile/editProfile/EditProfile.tsx | 4 +- .../ProjectInformationInput.tsx | 4 +- .../positionComponent/PositionComponent.tsx | 12 +- src/constants/admin/sidebar.ts | 11 +- src/constants/routes.ts | 3 +- src/constants/user/modalMessage.ts | 3 + src/hooks/admin/useAdminInquiry.ts | 2 +- src/hooks/admin/useAdminNotice.ts | 2 +- src/hooks/admin/useAdminTag.ts | 97 +++++++ src/hooks/admin/useGetAllInquiries.ts | 3 +- src/hooks/queries/keys.ts | 16 +- ...gSkillTag.ts => useSearchFilteringTags.ts} | 9 +- src/models/tags.ts | 7 +- src/pages/admin/adminTags/AdminTags.tsx | 3 - .../position/AdminPositionTagsPage.tsx | 5 + .../adminTags/skill/AdminSkillTagsPage.tsx | 5 + src/routes/AdminRoutes.tsx | 45 +++- 38 files changed, 909 insertions(+), 64 deletions(-) create mode 100644 src/api/admin/customerService/inquiry.api.ts create mode 100644 src/api/admin/customerService/notice.api.ts create mode 100644 src/api/admin/tag.api.ts create mode 100644 src/components/admin/adminTags/AdminTagCRUD.styled.ts create mode 100644 src/components/admin/adminTags/AdminTagCRUD.tsx create mode 100644 src/components/admin/adminTags/AdminTagsBasic.styled.ts create mode 100644 src/components/admin/adminTags/AdminTagsBasic.tsx create mode 100644 src/components/admin/adminTags/positions/AdminPositionItems.styled.ts create mode 100644 src/components/admin/adminTags/positions/AdminPositionItems.tsx create mode 100644 src/components/admin/adminTags/positions/AdminPositionTags.tsx create mode 100644 src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts create mode 100644 src/components/admin/adminTags/skills/AdminSkillTagItems.tsx create mode 100644 src/components/admin/adminTags/skills/AdminSkillTags.tsx create mode 100644 src/hooks/admin/useAdminTag.ts rename src/hooks/user/{useSearchFilteringSkillTag.ts => useSearchFilteringTags.ts} (86%) delete mode 100644 src/pages/admin/adminTags/AdminTags.tsx create mode 100644 src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx create mode 100644 src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx diff --git a/src/api/admin/customerService/inquiry.api.ts b/src/api/admin/customerService/inquiry.api.ts new file mode 100644 index 00000000..a6536a9c --- /dev/null +++ b/src/api/admin/customerService/inquiry.api.ts @@ -0,0 +1,61 @@ +import type { ApiCommonBasicType } from '../../../models/apiCommon'; +import type { + ApiAdminInquiry, + ApiAdminInquiryDetail, + InquiryAnswerBody, +} from '../../../models/inquiry'; +import { httpClient } from '../../http.api'; + +export const getAllInquiries = async () => { + try { + const response = await httpClient.get(`/inquiry`); + return response.data.data; + } catch (e) { + console.error('전체 문의 조회 에러', e); + throw e; + } +}; + +export const getInquiryDetail = async (id: string) => { + try { + const response = await httpClient.get( + `/inquiry/${id}` + ); + + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const postInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => { + try { + await httpClient.post(`/inquiry/${id}/answer`, { + answer, + }); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const patchInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => { + try { + await httpClient.patch(`/inquiry/${id}/answer`, { + answer, + }); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deleteInquiry = async (id: string) => { + try { + await httpClient.delete(`/inquiry/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/api/admin/customerService/notice.api.ts b/src/api/admin/customerService/notice.api.ts new file mode 100644 index 00000000..2455cc93 --- /dev/null +++ b/src/api/admin/customerService/notice.api.ts @@ -0,0 +1,30 @@ +import type { ApiCommonBasicType } from '../../../models/apiCommon'; +import type { WriteBody } from '../../../models/customerService'; +import { httpClient } from '../../http.api'; + +export const postNotice = async (formData: WriteBody) => { + try { + await httpClient.post(`/notice`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putNotice = async (id: string, formData: WriteBody) => { + try { + await httpClient.put(`/notice/${id}`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deleteNotice = async (id: string) => { + try { + await httpClient.delete(`/notice/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/api/admin/tag.api.ts b/src/api/admin/tag.api.ts new file mode 100644 index 00000000..7d1d467c --- /dev/null +++ b/src/api/admin/tag.api.ts @@ -0,0 +1,69 @@ +import type { ApiCommonBasicType } from '../../models/apiCommon'; +import type { TagFormType } from '../../models/tags'; +import { httpClient } from '../http.api'; + +export const postSkillTag = async (formData: FormData) => { + try { + await httpClient.post(`/skill-tag`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putSkillTag = async ({ + formData, + id, +}: { + formData: FormData; + id: number; +}) => { + try { + await httpClient.put(`/skill-tag/${id}`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deleteSkillTag = async (id: number) => { + try { + await httpClient.delete(`/skill-tag/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const postPositionTag = async (name: string) => { + try { + await httpClient.post(`/position-tag`, { name }); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putPositionTag = async ({ + name, + id, +}: { + name: string; + id: number; +}) => { + try { + await httpClient.put(`/position-tag/${id}`, { name }); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deletePositionTag = async (id: number) => { + try { + await httpClient.delete(`/position-tag/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/components/admin/adminTags/AdminTagCRUD.styled.ts b/src/components/admin/adminTags/AdminTagCRUD.styled.ts new file mode 100644 index 00000000..55b660eb --- /dev/null +++ b/src/components/admin/adminTags/AdminTagCRUD.styled.ts @@ -0,0 +1,84 @@ +import styled from 'styled-components'; +import { SendButton } from '../../user/customerService/inquiry/Inquiry.styled'; + +export const CRUDContainer = styled.form` + width: 100%; + height: 100%; +`; + +export const CRUDWrapper = styled.div` + width: 70%; + height: 100%; + display: flex; + gap: 1rem; + font-size: 1.2rem; + justify-content: space-between; +`; + +export const InfoContainer = styled.div` + display: flex; + height: 100%; + flex-direction: column; + gap: 1.5rem; + justify-content: center; + /* align-items: center; */ +`; + +export const CRUDButtonWrapper = styled.div` + display: grid; + gap: 1rem; +`; + +export const CRUDButton = styled(SendButton)` + height: 2.3rem; +`; + +export const CRUDTitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +export const CRUDTitleHead = styled.span``; + +export const CRUDTitle = styled.input` + border-bottom: 1px solid ${({ theme }) => theme.color.placeholder}; + padding-left: 0.3rem; + font-size: 1rem; +`; + +export const CRUDDefaultButton = styled.button` + svg { + width: 1rem; + height: 1rem; + } +`; + +export const CRUDImgWrapper = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +export const CRUDImgHead = styled.span``; + +export const CRUDImg = styled.img` + width: 3rem; + border: 1px solid ${({ theme }) => theme.color.grey}; +`; + +export const CRUDImgExplore = styled(SendButton)` + cursor: pointer; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + padding: 0.4rem 1rem; +`; + +export const CRUDImgExplain = styled.span` + max-width: 10rem; +`; + +export const CRUDImgInput = styled.input` + visibility: hidden; + width: 0; + height: 0; +`; diff --git a/src/components/admin/adminTags/AdminTagCRUD.tsx b/src/components/admin/adminTags/AdminTagCRUD.tsx new file mode 100644 index 00000000..f99c0935 --- /dev/null +++ b/src/components/admin/adminTags/AdminTagCRUD.tsx @@ -0,0 +1,254 @@ +import * as S from './AdminTagCRUD.styled'; +import defaultImg from './../../../assets/defaultImg.png'; +import React, { useEffect, useRef, useState } from 'react'; +import type { PositionTag, SkillTag, TagFormType } from '../../../models/tags'; +import Modal from '../../common/modal/Modal'; +import { useModal } from '../../../hooks/useModal'; +import { MODAL_MESSAGE } from '../../../constants/user/modalMessage'; +import { useSearchFilteringTags } from '../../../hooks/user/useSearchFilteringTags'; +import { XMarkIcon } from '@heroicons/react/24/outline'; + +interface TagState { + type: string; + label: string; + needImgFile: boolean; + handlePostTag: (params: T) => void; + handlePutTag: ({ params, id }: { params: T; id: number }) => void; + handleDeleteTag: (id: number) => void; +} + +interface AdminTagCRUDProps { + state: TagState; + itemId: number | null; + onGetItemId: (id: number | null) => void; +} + +interface FormDataType extends TagFormType { + preview: string; +} + +type Skill = Omit; + +type CRUDDataType = Skill | PositionTag; + +type SubmitButtonType = '등록' | '수정' | '삭제'; + +export default function AdminTagCRUD({ + state, + itemId, + onGetItemId, +}: AdminTagCRUDProps) { + const fileInputRef = useRef(null); + const textInputRef = useRef(null); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const { skillTagsData, positionTagsData } = useSearchFilteringTags(); + + const [buttonType, setButtonType] = useState('등록'); + const [formState, setFormState] = useState({ + name: '', + preview: '', + img: undefined, + }); + + const data: CRUDDataType = + state.type === 'skill' + ? skillTagsData.filter((list) => list.id === itemId)[0] + : positionTagsData.filter((list) => list.id === itemId)[0]; + + const handleSubmitTag = (e: React.FormEvent) => { + e.preventDefault(); + + const formData = new FormData(e.currentTarget as HTMLFormElement); + + const name = String(formData.get('name')); + + const isValid = { + name: formState.name.trim() !== '', + preview: formState.preview.trim() !== '', + }; + + if (!isValid.name) { + return handleModalOpen(MODAL_MESSAGE.emptyTag); + } + if (state.type === 'skill' && !isValid.preview && !itemId) { + return handleModalOpen(MODAL_MESSAGE.emptySkillImg); + } + + switch (buttonType) { + case '등록': + { + const duplication = + state.type === 'skill' + ? skillTagsData.filter((data) => data.name === name) + : positionTagsData.filter((data) => data.name === name); + if (duplication.length > 0) { + return handleModalOpen(MODAL_MESSAGE.duplicationTag); + } + if (state.type === 'skill') { + state.handlePostTag(formData as T); + } else { + state.handlePostTag(name as T); + } + } + break; + case '수정': + { + const duplication = + state.type === 'skill' + ? skillTagsData + .filter((data) => data.id !== itemId) + .filter((data) => data.name === name) + : positionTagsData + .filter((data) => data.id !== itemId) + .filter((data) => data.name === name); + if (duplication.length > 0) { + return handleModalOpen(MODAL_MESSAGE.duplicationTag); + } + if (!itemId) return; + if (state.type === 'skill') { + state.handlePutTag({ params: formData, id: itemId } as { + params: T; + id: number; + }); + } else { + state.handlePutTag({ params: name, id: itemId } as { + params: T; + id: number; + }); + } + } + + break; + case '삭제': + if (itemId) { + state.handleDeleteTag(itemId); + } + break; + default: + break; + } + handleClickReset(); + }; + + const handleChangeValue = (e: React.ChangeEvent) => { + const name = e.target.value; + setFormState((prev) => ({ ...prev, name })); + }; + + const handleChangeFile = (e: React.ChangeEvent) => { + const img = e.target.files?.[0]; + const preview = img ? URL.createObjectURL(img) : ''; + setFormState((prev) => ({ ...prev, preview, img })); + }; + + const handleClickReset = () => { + setFormState({ + name: '', + preview: '', + img: undefined, + }); + onGetItemId(null); + setTimeout(() => { + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + if (textInputRef.current) { + textInputRef.current.value = ''; + } + }, 1000); + }; + + const handleClickChangeButtonType = ( + e: React.MouseEvent + ) => { + const id = e.currentTarget.id as SubmitButtonType; + setButtonType(id); + }; + + useEffect(() => { + if (data) { + if (state.type === 'skill') { + const skillData = data as unknown as Skill; + if (skillData.img) { + const preview = skillData.img as string; + setFormState((prev) => ({ ...prev, name: data.name, preview })); + } + } else { + setFormState((prev) => ({ ...prev, name: data.name })); + } + } + }, [data, state]); + + useEffect(() => { + return () => { + if (formState.preview) { + URL.revokeObjectURL(formState.preview); + } + }; + }, [formState]); + + return ( + + + + + {state.label}: + + {itemId && ( + + + + )} + + {state.type === 'skill' && ( + + 이미지: + + + 파일찾기 + + + + )} + + + + {itemId ? '수정' : '등록'} + + {Boolean(itemId) && ( + + 삭제 + + )} + + + + {message} + + + ); +} diff --git a/src/components/admin/adminTags/AdminTagsBasic.styled.ts b/src/components/admin/adminTags/AdminTagsBasic.styled.ts new file mode 100644 index 00000000..dc5b271b --- /dev/null +++ b/src/components/admin/adminTags/AdminTagsBasic.styled.ts @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const Container = styled.main` + width: 100%; + display: flex; + gap: 3rem; + flex-direction: column; + align-items: center; +`; + +export const CRUDContainer = styled.section` + width: 90%; + min-height: 11rem; + border: 1px solid ${({ theme }) => theme.color.placeholder}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + display: flex; + justify-content: center; + padding: 2rem 3rem; +`; + +export const ItemContainer = styled.section` + width: 90%; + border: 1px solid ${({ theme }) => theme.color.placeholder}; + border-radius: ${({ theme }) => theme.borderRadius.large}; +`; diff --git a/src/components/admin/adminTags/AdminTagsBasic.tsx b/src/components/admin/adminTags/AdminTagsBasic.tsx new file mode 100644 index 00000000..671b9f7a --- /dev/null +++ b/src/components/admin/adminTags/AdminTagsBasic.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import * as S from './AdminTagsBasic.styled'; +import { useAdminSkillTag } from '../../../hooks/admin/useAdminTag'; +import { useLocation } from 'react-router-dom'; +import AdminTagCRUD from './AdminTagCRUD'; +import AdminSkillTagItems from './skills/AdminSkillTagItems'; +import type { TagFormType } from '../../../models/tags'; +import AdminPositionItems from './positions/AdminPositionItems'; + +export type TWitchTag = 'skill' | 'position'; + +export default function AdminTagsBasic() { + const location = useLocation(); + const pathname = location.pathname; + const witchTag: TWitchTag = pathname.includes('skill') ? 'skill' : 'position'; + + const { + postSkillTagMutate, + putSkillTagMutate, + deleteSkillTagMutate, + postPositionTagMutate, + putPositionTagMutate, + deletePositionTagMutate, + } = useAdminSkillTag(); + const [itemId, setItemId] = useState(null); + const selectTagId = itemId ? [itemId] : []; + + const tagState = { + skill: { + type: 'skill', + label: '스킬', + needImgFile: true, + handlePostTag: (formData: FormData) => + postSkillTagMutate.mutate(formData), + handlePutTag: ({ params, id }: { params: FormData; id: number }) => + putSkillTagMutate.mutate({ formData: params, id }), + handleDeleteTag: (id: number) => deleteSkillTagMutate.mutate(id), + }, + position: { + type: 'position', + label: '포지션', + needImgFile: false, + handlePostTag: (name: string) => postPositionTagMutate.mutate({ name }), + handlePutTag: ({ params, id }: { params: string; id: number }) => + putPositionTagMutate.mutate({ name: params, id }), + handleDeleteTag: (id: number) => deletePositionTagMutate.mutate(id), + }, + }; + + const handleGetItemId = (id: number | null) => { + setItemId(id); + }; + + return ( + + + {witchTag === 'skill' ? ( + + state={tagState.skill} + itemId={itemId} + onGetItemId={handleGetItemId} + /> + ) : ( + + state={tagState.position} + itemId={itemId} + onGetItemId={handleGetItemId} + /> + )} + + + {witchTag === 'skill' ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/components/admin/adminTags/positions/AdminPositionItems.styled.ts b/src/components/admin/adminTags/positions/AdminPositionItems.styled.ts new file mode 100644 index 00000000..8bb7c5c6 --- /dev/null +++ b/src/components/admin/adminTags/positions/AdminPositionItems.styled.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Container = styled.section` + padding: 2rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +`; diff --git a/src/components/admin/adminTags/positions/AdminPositionItems.tsx b/src/components/admin/adminTags/positions/AdminPositionItems.tsx new file mode 100644 index 00000000..395808d5 --- /dev/null +++ b/src/components/admin/adminTags/positions/AdminPositionItems.tsx @@ -0,0 +1,26 @@ +import { useSearchFilteringTags } from '../../../../hooks/user/useSearchFilteringTags'; +import PositionButton from '../../../common/positionButton/PositionButton'; +import * as S from './AdminPositionItems.styled'; + +interface AdminPositionItemsProps { + onGetItemId: (id: number) => void; +} + +export default function AdminPositionItems({ + onGetItemId, +}: AdminPositionItemsProps) { + const { positionTagsData } = useSearchFilteringTags(); + + return ( + + {positionTagsData.map((list) => ( + onGetItemId(list.id)} + /> + ))} + + ); +} diff --git a/src/components/admin/adminTags/positions/AdminPositionTags.tsx b/src/components/admin/adminTags/positions/AdminPositionTags.tsx new file mode 100644 index 00000000..5d4fb814 --- /dev/null +++ b/src/components/admin/adminTags/positions/AdminPositionTags.tsx @@ -0,0 +1,5 @@ +import AdminTagsBasic from '../AdminTagsBasic'; + +export default function AdminPositionTags() { + return ; +} diff --git a/src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts b/src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts new file mode 100644 index 00000000..505a0efe --- /dev/null +++ b/src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts @@ -0,0 +1,3 @@ +import styled from 'styled-components'; + +export const SkillTagItemWrapper = styled.section``; diff --git a/src/components/admin/adminTags/skills/AdminSkillTagItems.tsx b/src/components/admin/adminTags/skills/AdminSkillTagItems.tsx new file mode 100644 index 00000000..e3385a79 --- /dev/null +++ b/src/components/admin/adminTags/skills/AdminSkillTagItems.tsx @@ -0,0 +1,30 @@ +import * as S from './AdminSkillTagItems.styled'; +import SkillTagBox from '../../../common/skillTagBox/SkillTagBox'; + +interface AdminSKillTagItemsProps { + onGetItemId: (id: number) => void; + selectTagId: number[]; +} + +export default function AdminSkillTagItems({ + onGetItemId, + selectTagId, +}: AdminSKillTagItemsProps) { + const handleClickGetId = (e: React.MouseEvent) => { + e.stopPropagation(); + const target = e.target as HTMLElement; + + const id = Number( + target.dataset.id || target.closest('[data-id]')?.getAttribute('data-id') + ); + + if (!id) return; + onGetItemId(id); + }; + + return ( + + + + ); +} diff --git a/src/components/admin/adminTags/skills/AdminSkillTags.tsx b/src/components/admin/adminTags/skills/AdminSkillTags.tsx new file mode 100644 index 00000000..ee314178 --- /dev/null +++ b/src/components/admin/adminTags/skills/AdminSkillTags.tsx @@ -0,0 +1,5 @@ +import AdminTagsBasic from '../AdminTagsBasic'; + +export default function AdminSkillTags() { + return ; +} diff --git a/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx b/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx index 19caf835..7502472f 100644 --- a/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx +++ b/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx @@ -18,7 +18,8 @@ const iconMap = { notice: , faq: , banner: , - tags: , + skillTags: , + positionTags: , allUser: , reports: , inquiries: , diff --git a/src/components/common/positionButton/PositionButton.styled.ts b/src/components/common/positionButton/PositionButton.styled.ts index 86d6acdd..58d284ec 100644 --- a/src/components/common/positionButton/PositionButton.styled.ts +++ b/src/components/common/positionButton/PositionButton.styled.ts @@ -1,7 +1,5 @@ import styled, { css } from 'styled-components'; -export const Container = styled.div``; - export const PositionButton = styled.button<{ $isSelected: boolean; $isHover: boolean; diff --git a/src/components/common/positionButton/PositionButton.tsx b/src/components/common/positionButton/PositionButton.tsx index 18384464..8a0508f5 100644 --- a/src/components/common/positionButton/PositionButton.tsx +++ b/src/components/common/positionButton/PositionButton.tsx @@ -2,29 +2,28 @@ import * as S from './PositionButton.styled'; interface PositionButtonProps { position: string; - onClick?: (e: React.MouseEvent) => void; + onClickSelect?: (e: React.MouseEvent) => void; isSelected?: boolean; isHover?: boolean; - fontSize: boolean; + fontSize?: boolean; } export default function PositionButton({ position, - onClick, + onClickSelect, isSelected = false, isHover = false, fontSize = false, }: PositionButtonProps) { return ( - - - {position} - - + onClickSelect?.(e)} + > + {position} + ); } diff --git a/src/components/common/skillTagBox/SkillTagBox.tsx b/src/components/common/skillTagBox/SkillTagBox.tsx index d59aa050..50a58037 100644 --- a/src/components/common/skillTagBox/SkillTagBox.tsx +++ b/src/components/common/skillTagBox/SkillTagBox.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { useSearchFilteringSkillTag } from '../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../hooks/user/useSearchFilteringTags'; import SkillTag from './skillTag/SkillTag'; import * as S from './SkillTagBox.styled'; import { ArrowUturnLeftIcon } from '@heroicons/react/24/outline'; import { useSaveSearchFiltering } from '../../../hooks/user/useSaveSearchFiltering'; export interface SkillTagBoxProps { - width: string; + width?: string; onHandleSkillTagReset?: React.MouseEventHandler; selectedTag?: number[]; isMain?: boolean; @@ -14,13 +14,13 @@ export interface SkillTagBoxProps { } export default function SkillTagBox({ - width, + width = '100%', onHandleSkillTagReset, selectedTag, isMain = false, isCreate = false, }: SkillTagBoxProps) { - const { skillTagsData } = useSearchFilteringSkillTag(); + const { skillTagsData } = useSearchFilteringTags(); const { searchFilters } = useSaveSearchFiltering(); const searchFiltersSkillTag = searchFilters.skillTag; @@ -36,11 +36,12 @@ export default function SkillTagBox({ skillTagData={skillTagData} key={`skillTagBox-${skillTagData.id}`} $select={ - (isMain && + selectedTag?.includes(skillTagData.id) || + ((isMain && searchFiltersSkillTag?.includes(skillTagData.id)) || (isCreate && selectedTag?.includes(skillTagData.id)) ? true - : false + : false) } /> ))} diff --git a/src/components/common/skillTagBox/skillTag/SkillTag.tsx b/src/components/common/skillTagBox/skillTag/SkillTag.tsx index ea209e0e..a7239736 100644 --- a/src/components/common/skillTagBox/skillTag/SkillTag.tsx +++ b/src/components/common/skillTagBox/skillTag/SkillTag.tsx @@ -14,6 +14,7 @@ export default function SkillTag({ skillTagData, $select }: SkillTagProps) { image={skillTagData.img} skillTag={skillTagData.name} $select={$select} + updatedAt={skillTagData.updatedAt} skillTagId={skillTagData.id} /> {skillTagData.name} diff --git a/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx b/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx index 924444a3..d52e4db9 100644 --- a/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx +++ b/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx @@ -3,18 +3,24 @@ import * as S from './SkillTagImg.styled'; export interface SkillTagImgProps { image: string; skillTag: string; + updatedAt: string; $select?: boolean; skillTagId?: number; } export default function SkillTagImg({ image, skillTag, + updatedAt, $select, skillTagId, }: SkillTagImgProps) { return ( - + ); } diff --git a/src/components/user/home/projectCardLists/cardList/CardList.tsx b/src/components/user/home/projectCardLists/cardList/CardList.tsx index 4975a5bb..82c0f65a 100644 --- a/src/components/user/home/projectCardLists/cardList/CardList.tsx +++ b/src/components/user/home/projectCardLists/cardList/CardList.tsx @@ -28,7 +28,6 @@ export default function CardList({ list }: CardListProps) { ))} {list.positions.length > listPositionTag.length && ( diff --git a/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx b/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx index 9288dbcd..bd434ad1 100644 --- a/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx +++ b/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx @@ -3,14 +3,14 @@ import * as S from './FilteringContents.styled'; import beginner from '../../../../../assets/beginner.svg'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; import React, { useState } from 'react'; -import { useSearchFilteringSkillTag } from '../../../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../../../hooks/user/useSearchFilteringTags'; import { useOutsideClick } from '../../../../../hooks/user/useOutsideClick'; import { useSaveSearchFiltering } from '../../../../../hooks/user/useSaveSearchFiltering'; import { SEARCH_FILTERING_DEFAULT_VALUE } from '../../../../../constants/user/homeConstants'; import SkillTagBox from '../../../../common/skillTagBox/SkillTagBox'; export default function FilteringContents() { - const { positionTagsData, methodTagsData } = useSearchFilteringSkillTag(); + const { positionTagsData, methodTagsData } = useSearchFilteringTags(); const { searchFilters, handleUpdateFilters } = useSaveSearchFiltering(); const [skillTagButtonToggle, setSkillTagButtonToggle] = useState(false); @@ -18,7 +18,7 @@ export default function FilteringContents() { setSkillTagButtonToggle((prev) => !prev); }; - const handleSkillTagFilterClick = (e: React.MouseEvent) => { + const handleSkillTagFilterClick = (e: React.MouseEvent) => { e.stopPropagation(); const target = e.target as HTMLElement; diff --git a/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx b/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx index d89ead01..eb89563b 100644 --- a/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx +++ b/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx @@ -10,7 +10,7 @@ import { SquaresPlusIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { useNavigate, useOutletContext } from 'react-router-dom'; import MyProfileWrapper from '../MyProfileWrapper'; import type { UserInfo } from '../../../../../models/userInfo'; -import { useSearchFilteringSkillTag } from '../../../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../../../hooks/user/useSearchFilteringTags'; import { useEditMyProfileInfo } from '../../../../../hooks/user/useMyInfo'; import useNickNameVerification from '../../../../../hooks/user/useNicknameVerification'; import { ROUTES } from '../../../../../constants/routes'; @@ -34,7 +34,7 @@ export default function EditProfile() { scrollRef: React.RefObject; handleModalOpen: (message: string) => void; } = useOutletContext(); - const { skillTagsData, positionTagsData } = useSearchFilteringSkillTag(); + const { skillTagsData, positionTagsData } = useSearchFilteringTags(); const { editMyProfile } = useEditMyProfileInfo(handleModalOpen); const { nicknameMessage, handleDuplicationNickname } = useNickNameVerification(); diff --git a/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx b/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx index 6edd443e..b3a89955 100644 --- a/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx +++ b/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx @@ -6,7 +6,7 @@ import * as S from './ProjectInformationInput.styled'; import Input from '../inputComponent/InputComponent'; import type { ProjectDetailPlusExtended } from '../../../../models/projectDetail'; import type { CreateProjectFormValues } from '../../../../models/createProject'; -import { useSearchFilteringSkillTag } from '../../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../../hooks/user/useSearchFilteringTags'; import { PROJECT_DATA } from '../../../../constants/user/projectConstants'; interface ProjectInformationProps { @@ -22,7 +22,7 @@ const ProjectInformationInput = ({ setValue, apiData, }: ProjectInformationProps) => { - const { positionTagsData, methodTagsData } = useSearchFilteringSkillTag(); + const { positionTagsData, methodTagsData } = useSearchFilteringTags(); return ( <> diff --git a/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx b/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx index ad267d8e..00d0e4a1 100644 --- a/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx +++ b/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import useTagSelectors from '../../../../../hooks/user/ProjectHooks/useTagSelectors'; import type { CreateProjectFormValues } from '../../../../../models/createProject'; import type { PositionTag } from '../../../../../models/tags'; @@ -27,6 +28,13 @@ const MozipCategoryComponent = ({ fieldName: 'position', }); + const handleClickSelect = ( + e: React.MouseEvent, + idx: number + ) => { + handleClick(e, idx + 1); + }; + return ( @@ -36,9 +44,7 @@ const MozipCategoryComponent = ({ ) => - handleClick(e, idx + 1) - } + onClickSelect={(e) => handleClickSelect(e, idx)} key={idx + 1} isHover={true} fontSize={true} diff --git a/src/constants/admin/sidebar.ts b/src/constants/admin/sidebar.ts index 541e8f49..40df363e 100644 --- a/src/constants/admin/sidebar.ts +++ b/src/constants/admin/sidebar.ts @@ -30,9 +30,14 @@ export const SIDEBAR_LIST = { router: ADMIN_ROUTE.banner, }, { - name: 'tags', - title: '태그관리', - router: ADMIN_ROUTE.tags, + name: 'skillTags', + title: '스킬 태그', + router: ADMIN_ROUTE.skillTags, + }, + { + name: 'positionTags', + title: '포지션 태그', + router: ADMIN_ROUTE.positionTags, }, ], user: [ diff --git a/src/constants/routes.ts b/src/constants/routes.ts index ddadceb3..18c66d4d 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -37,7 +37,8 @@ export const ADMIN_ROUTE = { notice: 'notice', faq: 'faq', banner: 'banner', - tags: 'tags', + skillTags: 'skill-tags', + positionTags: 'position-tags', users: 'users', reports: 'reports', inquiries: 'inquiries', diff --git a/src/constants/user/modalMessage.ts b/src/constants/user/modalMessage.ts index 0fa52b62..0556fbe4 100644 --- a/src/constants/user/modalMessage.ts +++ b/src/constants/user/modalMessage.ts @@ -27,4 +27,7 @@ export const MODAL_MESSAGE = { alreadyApply: '이미 참여한/지원하신 공고 입니다.', noMemberToEvaluate: '평가 할 멤버가 없습니다.', noTagsData: '대표 스킬/포지션을 입력 하셔야 사용할 수 있습니다.', + duplicationTag: '이미 존재하는 태그입니다.', + emptyTag: '태그명을 입력하세요.', + emptySkillImg: '스킬 이미지를 추가하세요.', } as const; diff --git a/src/hooks/admin/useAdminInquiry.ts b/src/hooks/admin/useAdminInquiry.ts index ebb7d3a1..a24e311c 100644 --- a/src/hooks/admin/useAdminInquiry.ts +++ b/src/hooks/admin/useAdminInquiry.ts @@ -4,7 +4,7 @@ import { getInquiryDetail, patchInquiryAnswer, postInquiryAnswer, -} from '../../api/admin/customerService/Inquiry.api'; +} from '../../api/admin/customerService/inquiry.api'; import type { InquiryAnswerBody } from '../../models/inquiry'; import { AxiosError } from 'axios'; import { CustomerService } from '../queries/keys'; diff --git a/src/hooks/admin/useAdminNotice.ts b/src/hooks/admin/useAdminNotice.ts index e06a5cac..e4345710 100644 --- a/src/hooks/admin/useAdminNotice.ts +++ b/src/hooks/admin/useAdminNotice.ts @@ -3,7 +3,7 @@ import { deleteNotice, postNotice, putNotice, -} from '../../api/admin/customerService/Notice.api'; +} from '../../api/admin/customerService/notice.api'; import { AxiosError } from 'axios'; import { CustomerService } from '../queries/keys'; import { useNavigate } from 'react-router-dom'; diff --git a/src/hooks/admin/useAdminTag.ts b/src/hooks/admin/useAdminTag.ts new file mode 100644 index 00000000..3fc12ec4 --- /dev/null +++ b/src/hooks/admin/useAdminTag.ts @@ -0,0 +1,97 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + deletePositionTag, + deleteSkillTag, + postPositionTag, + postSkillTag, + putPositionTag, + putSkillTag, +} from '../../api/admin/tag.api'; +import { AxiosError } from 'axios'; +import type { TagFormType } from '../../models/tags'; +import { Tag } from '../queries/keys'; + +export const useAdminSkillTag = () => { + const queryClient = useQueryClient(); + + const postSkillTagMutate = useMutation({ + mutationFn: (formData) => postSkillTag(formData), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.skillTag, + }); + }, + }); + + const putSkillTagMutate = useMutation< + void, + AxiosError, + { + formData: FormData; + id: number; + } + >({ + mutationFn: ({ formData, id }) => putSkillTag({ formData, id }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.skillTag, + }); + }, + }); + + const deleteSkillTagMutate = useMutation({ + mutationFn: (id: number) => deleteSkillTag(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.skillTag, + }); + }, + }); + + const postPositionTagMutate = useMutation< + void, + AxiosError, + Pick + >({ + mutationFn: ({ name }) => postPositionTag(name), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.positionTag, + }); + }, + }); + + const putPositionTagMutate = useMutation< + void, + AxiosError, + { + name: string; + id: number; + } + >({ + mutationFn: ({ name, id }) => putPositionTag({ name, id }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.positionTag, + }); + }, + }); + + const deletePositionTagMutate = useMutation({ + mutationFn: (id: number) => deletePositionTag(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.positionTag, + }); + }, + }); + + return { + postSkillTagMutate, + putSkillTagMutate, + deleteSkillTagMutate, + postPositionTagMutate, + putPositionTagMutate, + deletePositionTagMutate, + }; +}; diff --git a/src/hooks/admin/useGetAllInquiries.ts b/src/hooks/admin/useGetAllInquiries.ts index 8f1d21b9..5f7fc046 100644 --- a/src/hooks/admin/useGetAllInquiries.ts +++ b/src/hooks/admin/useGetAllInquiries.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { Inquiries } from '../queries/keys'; -import { getAllInquiries } from '../../api/admin/customerService/Inquiry.api'; +import { getAllInquiries } from '../../api/admin/customerService/inquiry.api'; export const useGetAllInquiries = () => { const { @@ -10,7 +10,6 @@ export const useGetAllInquiries = () => { } = useQuery({ queryKey: [Inquiries.allInquiries], queryFn: () => getAllInquiries(), - select: (allInquiries) => allInquiries.slice(0, 5), }); return { allInquiriesData, isLoading, isFetching }; diff --git a/src/hooks/queries/keys.ts b/src/hooks/queries/keys.ts index 8caeccc2..bb4b97a4 100644 --- a/src/hooks/queries/keys.ts +++ b/src/hooks/queries/keys.ts @@ -47,25 +47,31 @@ export const ProjectMemberListEval = { export const ActivityLog = { myComments: ['MyComments'], myInquiries: ['MyInquiries'], -}; +} as const; export const Inquiries = { allInquiries: ['AllInquiries'], -}; +} as const; export const CustomerService = { faq: 'faq', notice: 'notice', noticeDetail: 'noticeDetail', inquiryDetail: 'inquiryDetail', -}; +} as const; export const ReportData = { allReports: ['AllReports'], -}; +} as const; export const UserData = { allUser: ['AllUser'], allUserPreview: ['AllUserPreview'], userInfo: ['userInfo'], -}; +} as const; + +export const Tag = { + skillTag: ['skillTagsData'], + positionTag: ['positionsData'], + method: ['fetchMethodTag'], +} as const; diff --git a/src/hooks/user/useSearchFilteringSkillTag.ts b/src/hooks/user/useSearchFilteringTags.ts similarity index 86% rename from src/hooks/user/useSearchFilteringSkillTag.ts rename to src/hooks/user/useSearchFilteringTags.ts index c5a87130..2dcbe5b4 100644 --- a/src/hooks/user/useSearchFilteringSkillTag.ts +++ b/src/hooks/user/useSearchFilteringTags.ts @@ -6,8 +6,9 @@ import { getPositionTag, getSkillTag, } from '../../api/projectSearchFiltering.api'; +import { Tag } from '../queries/keys'; -export const useSearchFilteringSkillTag = () => { +export const useSearchFilteringTags = () => { const [skillTagsData, setSkillTagsData] = useState([]); const [positionTagsData, setPositionTagsData] = useState([]); const [methodTagsData, setMethodTagsData] = useState([]); @@ -15,19 +16,19 @@ export const useSearchFilteringSkillTag = () => { const queries = useQueries({ queries: [ { - queryKey: ['skillTagsData', skillTagsData], + queryKey: Tag.skillTag, queryFn: () => getSkillTag(), staleTime: Infinity, gcTime: Infinity, }, { - queryKey: ['positionsData', positionTagsData], + queryKey: Tag.positionTag, queryFn: () => getPositionTag(), staleTime: Infinity, gcTime: Infinity, }, { - queryKey: ['fetchMethodTag', methodTagsData], + queryKey: Tag.method, queryFn: () => getMethodTag(), staleTime: Infinity, gcTime: Infinity, diff --git a/src/models/tags.ts b/src/models/tags.ts index bb7922b2..5d206b09 100644 --- a/src/models/tags.ts +++ b/src/models/tags.ts @@ -4,7 +4,7 @@ export interface SkillTag { id: number; name: string; img: string; - createdAt: string; + updatedAt: string; } export interface PositionTag { @@ -30,3 +30,8 @@ export interface ApiPositionTag extends ApiCommonType { export interface ApiMethodTag extends ApiCommonType { data: MethodTag[] | null; } + +export interface TagFormType { + name: string; + img?: File | undefined; +} diff --git a/src/pages/admin/adminTags/AdminTags.tsx b/src/pages/admin/adminTags/AdminTags.tsx deleted file mode 100644 index b8bd4f27..00000000 --- a/src/pages/admin/adminTags/AdminTags.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AdminTags() { - return
; -} diff --git a/src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx b/src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx new file mode 100644 index 00000000..45b2d834 --- /dev/null +++ b/src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx @@ -0,0 +1,5 @@ +import CommonAdminPage from '../../CommonAdminPage'; + +export default function AdminPositionTagsPage() { + return ; +} diff --git a/src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx b/src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx new file mode 100644 index 00000000..67a6d4a9 --- /dev/null +++ b/src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx @@ -0,0 +1,5 @@ +import CommonAdminPage from '../../CommonAdminPage'; + +export default function AdminSkillTagsPage() { + return ; +} diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index 91572af7..a961e4fa 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -2,13 +2,8 @@ import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import { lazy, Suspense } from 'react'; import { ADMIN_ROUTE } from '../constants/routes'; import ProtectAdminRoute from './ProtectAdminRoute'; -import AdminUserDetail from '../components/admin/adminUserDetail/AdminUserDetail'; -import UserProjects from '../components/user/userPage/userProjectList/UserProjectList'; -import Profile from '../components/user/mypage/myProfile/profile/Profile'; -import { Navigate } from 'react-router-dom'; -import ActivityLog from '../components/user/mypage/activityLog/ActivityLog'; -import Notifications from '../components/user/mypage/notifications/Notifications'; import { Spinner } from '../components/common/loadingSpinner/LoadingSpinner.styled'; +import { Navigate } from 'react-router-dom'; const Sidebar = lazy( () => import('../components/common/admin/sidebar/AdminSidebar') @@ -25,6 +20,21 @@ const NoticeWrite = lazy( const NoticeDetail = lazy( () => import('../pages/admin/adminNoticeDetail/AdminNoticeDetail') ); +const AdminUserDetail = lazy( + () => import('../components/admin/adminUserDetail/AdminUserDetail') +); +const Profile = lazy( + () => import('../components/user/mypage/myProfile/profile/Profile') +); +const ActivityLog = lazy( + () => import('../components/user/mypage/activityLog/ActivityLog') +); +const Notifications = lazy( + () => import('../components/user/mypage/notifications/Notifications') +); +const UserProjects = lazy( + () => import('../components/user/userPage/userProjectList/UserProjectList') +); const FAQ = lazy(() => import('../pages/admin/adminFAQ/AdminFAQ')); const FAQList = lazy( () => import('../pages/admin/adminFAQ/adminFAQList/AdminFAQListPage') @@ -33,7 +43,18 @@ const FAQWrite = lazy( () => import('../pages/admin/adminFAQ/adminFAQWrite/AdminFAQWritePage') ); const Banner = lazy(() => import('../pages/admin/adminBanner/AdminBanner')); -const Tags = lazy(() => import('../pages/admin/adminTags/AdminTags')); +const SkillTagPage = lazy( + () => import('../pages/admin/adminTags/skill/AdminSkillTagsPage') +); +const SkillTags = lazy( + () => import('../components/admin/adminTags/skills/AdminSkillTags') +); +const PositionTagPage = lazy( + () => import('../pages/admin/adminTags/position/AdminPositionTagsPage') +); +const PositionTags = lazy( + () => import('../components/admin/adminTags/positions/AdminPositionTags') +); const AdminUser = lazy(() => import('../pages/admin/adminUser/AdminUser')); const Reports = lazy(() => import('../pages/admin/adminReports/AdminReports')); const Inquiries = lazy( @@ -133,8 +154,14 @@ export const AdminRoutes = () => { element: , }, { - path: ADMIN_ROUTE.tags, - element: , + path: ADMIN_ROUTE.skillTags, + element: , + children: [{ index: true, element: }], + }, + { + path: ADMIN_ROUTE.positionTags, + element: , + children: [{ index: true, element: }], }, { path: ADMIN_ROUTE.users, From 1a018d0e7da68340b2d011bd52bca0aaaa00941c Mon Sep 17 00:00:00 2001 From: Cho SeungYeon <111514472+layout-SY@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:40:06 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feat=20:=20"=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80"=EA=B0=80=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20id=EB=A5=BC=20=EC=BF=BC=EB=A6=AC=EC=9D=98?= =?UTF-8?q?=20enabled=20=EC=98=B5=EC=85=98=20=EC=A1=B0=EA=B1=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/admin/useGetUserInfo.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/hooks/admin/useGetUserInfo.ts b/src/hooks/admin/useGetUserInfo.ts index a1fbc129..b109461f 100644 --- a/src/hooks/admin/useGetUserInfo.ts +++ b/src/hooks/admin/useGetUserInfo.ts @@ -1,17 +1,14 @@ import { useQuery } from '@tanstack/react-query'; -import useAuthStore from '../../store/authStore'; import { ApiUserInfo } from '../../models/userInfo'; import { getUserInfo } from '../../api/userpage.api'; import { userInfoKey } from '../queries/keys'; const useGetUserInfo = (id: number) => { - const isLoggedIn = useAuthStore.getState().isLoggedIn; - const { data, isLoading, isFetching } = useQuery({ queryKey: [userInfoKey.userProfile, id], queryFn: () => getUserInfo(id), staleTime: 1 * 60 * 1000, - enabled: isLoggedIn, + enabled: !!id, }); return { userData: data?.data, isLoading, isFetching };