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
1 change: 0 additions & 1 deletion src/api/activityLog.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ApiAllInquiries } from '../models/admin/mainPreview';
import type { ApiMyComments, ApiMyInquiries } from './../models/activityLog';
import { httpClient } from './http.api';

Expand Down
2 changes: 1 addition & 1 deletion src/api/admin/customerService/FAQ.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiCommonBasicType } from '../../../models/apiCommon';
import type { ApiCommonBasicType } from '../../../models/apiCommon';
import type { ApiFAQDetail, WriteBody } from '../../../models/customerService';
import { httpClient } from '../../http.api';

Expand Down
48 changes: 48 additions & 0 deletions src/api/admin/user.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ApiUserApplicantsData } from '../../models/admin/userDetail/userDetail.applicants';
import type { ApiUserProjectDataResponse } from '../../models/admin/userDetail/userProjectData';
import { httpClient } from '../http.api';

export const postBanUser = async (id: string) => {
try {
await httpClient.post(`/ban/${id}`);
} catch (e) {
console.error(e);
throw e;
}
};

export const postWarningUser = async (id: string) => {
try {
await httpClient.post(`/warning/${id}`);
} catch (e) {
console.error(e);
throw e;
}
};

export const getUserApplicants = async (
projectId: number,
applicantId: number
) => {
try {
const response = await httpClient.get<ApiUserApplicantsData>(
`/admin/project/${projectId}/full?applicantId=${applicantId}`
);
return response.data;
} catch (e) {
console.error(e);
throw e;
}
};

export const getUserProjectData = async (userId: number) => {
try {
const response = await httpClient.get<ApiUserProjectDataResponse>(
`/users/${userId}/projects`
);
return response.data.data.appliedProjects;
} catch (e) {
console.error(e);
throw e;
}
};
14 changes: 14 additions & 0 deletions src/api/admin/userActivity.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { httpClient } from '../http.api';
import type { ApiUserActivityResponse } from '../../models/admin/userDetail/userActivity';

export const getUserActivityData = async (userId: number) => {
try {
const response = await httpClient.get<ApiUserActivityResponse>(
`/users/${userId}/activities`
);
return response.data;
} catch (e) {
console.error(e);
throw e;
}
};
2 changes: 1 addition & 1 deletion src/api/mypage.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const getMyAppliedStatusList = async () => {
'/user/applications'
);

return response.data;
return response.data.data;
} catch (error) {
console.error('내가 지원한 프로젝트 리스트: ', error);
throw error;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useOutletContext } from 'react-router-dom';
import * as S from './AdminInquiryAnswerWrite.styled';
import React, { useEffect, useRef, useState } from 'react';
import { InquiryAnswerBody } from '../../../models/inquiry';
import type { InquiryAnswerBody } from '../../../models/inquiry';
import { UseMutationResult } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import Modal from '../../common/modal/Modal';
Expand Down
34 changes: 14 additions & 20 deletions src/components/admin/adminUserDetail/AdminUserDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ 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';
import useGetUserProjectData from '../../../hooks/admin/useGetUserProjectData';
import type { TabKey } from '../../../models/admin/userDetail/routing';

const AdminUserDetail = () => {
const { userId } = useParams();
const { userData, isLoading, isFetching } = useGetUserInfo(Number(userId));
const {
userData: projectData,
isLoading: projectLoading,
isFetching: projectFetching,
} = useGetUserProjectData(Number(userId));

if (isLoading || isFetching) {
if (isLoading || isFetching || projectLoading || projectFetching) {
return (
<S.Spinner>
<Spinner />
Expand All @@ -34,9 +39,9 @@ const AdminUserDetail = () => {
icon: React.ReactNode;
}[] = [
{
key: 'basic',
key: 'profile',
label: '기본 정보',
path: `/admin/users/${userId}/${ADMIN_ROUTE.basic}`,
path: `/admin/users/${userId}`,
icon: <InformationCircleIcon width='17px' height='17px' />,
},
{
Expand All @@ -46,23 +51,11 @@ const AdminUserDetail = () => {
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}`,
key: 'projects',
label: '지원/참여/기획한 프로젝트',
path: `/admin/users/${userId}/${ADMIN_ROUTE.projects}`,
icon: <UserGroupIcon width='17px' height='17px' />,
},
{
key: 'applied',
label: '지원한 프로젝트',
path: `/admin/users/${userId}/${ADMIN_ROUTE.appliedProject}`,
icon: <ClipboardDocumentListIcon width='17px' height='17px' />,
},
];

return (
Expand All @@ -88,6 +81,7 @@ const AdminUserDetail = () => {
<Outlet
context={{
userInfoData: userData,
projectData,
}}
/>
</S.DetailContent>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/sidebar/Sidebar.styled.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from 'styled-components';

export const Container = styled.div<{ $isAdmin: boolean }>`
export const Container = styled.div<{ $isAdmin: boolean | undefined }>`
display: flex;
flex-direction: column;
border: 2px solid #f0f0f0;
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ interface SidebarProps {
const Sidebar = ({ menuItems, profileImage, nickname }: SidebarProps) => {
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const location = useLocation();
const isAdmin = location.pathname.includes('/admin');
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(() => {
Expand Down
25 changes: 16 additions & 9 deletions src/components/user/mypage/ContentTab.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import * as S from './ContentTab.styled';
import { Link, Outlet, useLocation } from 'react-router-dom';
import { ROUTES } from '../../../constants/routes';
import ScrollWrapper from './ScrollWrapper';
import MovedInquiredLink from '../customerService/MoveInquiredLink';
import { ADMIN_ROUTE, ROUTES } from '../../../constants/routes';

interface Filter {
title: string;
Expand All @@ -19,29 +19,35 @@ interface ContentProps {

export default function ContentTab({ filter, $justifyContent }: ContentProps) {
const { pathname } = useLocation();
const [filterId, setFilterId] = useState<number>();
const isAdmin = pathname.includes('/admin');
const [filterId, setFilterId] = useState<number>();

function handleChangeId(id: number) {
setFilterId(id);
}
useEffect(() => {
if (
pathname.includes(ROUTES.notificationsAppliedProjects) ||
pathname.includes(ROUTES.activityInquiries)
pathname.includes(ROUTES.activityInquiries) ||
pathname.includes(ADMIN_ROUTE.appliedProject)
) {
return setFilterId(1);
} else if (pathname.includes(ROUTES.notificationsCheckedApplicants)) {
} else if (
pathname.includes(ROUTES.notificationsCheckedApplicants) ||
pathname.includes(ADMIN_ROUTE.joinedProject)
) {
return setFilterId(2);
} else if (
pathname.includes(`${ROUTES.myPageNotifications}/${ROUTES.comments}`)
pathname.includes(`${ROUTES.myPageNotifications}/${ROUTES.comments}`) ||
pathname.includes(ADMIN_ROUTE.createdProject)
) {
return setFilterId(3);
} else {
return setFilterId(0);
}
}, [setFilterId, pathname]);

function handleChangeId(id: number) {
setFilterId(id);
}

return (
<S.Container>
<S.FilterWrapper $justifyContent={$justifyContent}>
Expand All @@ -51,6 +57,7 @@ export default function ContentTab({ filter, $justifyContent }: ContentProps) {
to={filter.url}
onClick={() => handleChangeId(filter.id as number)}
>
{' '}
<S.WrapperTitle $selected={filter?.id === filterId}>
<S.FilterTitle>{filter.title}</S.FilterTitle>
</S.WrapperTitle>
Expand All @@ -64,7 +71,7 @@ export default function ContentTab({ filter, $justifyContent }: ContentProps) {
</S.WrapperButton>
<ScrollWrapper $height='10%'>
<S.FilterContainer>
<Outlet />
<Outlet context={{ filterId }} />
</S.FilterContainer>
</ScrollWrapper>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/user/mypage/activityLog/ActivityLog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useLocation } from 'react-router-dom';
import {
ACTIVITY_FILTER,
ACTIVITY_FILTER_ADMIN,
} from '../../../../constants/user/myPageFilter';
import ContentTab from '../ContentTab';
import { useLocation } from 'react-router-dom';

export default function ActivityLog() {
const { pathname } = useLocation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import Spinner from '../../Spinner';
import CommentActivity from './commentActivity/CommentActivity';
import * as S from './CommentsActivity.styled';
import NoContent from '../../../../common/noContent/NoContent';
import { useGetMyComments } from '../../../../../hooks/user/useGetMyComments';
import useGetUserActivity from '../../../../../hooks/admin/useGetAllUserActivity';
import { useParams } from 'react-router-dom';
import type { MyComments } from '../../../../../models/activityLog';
import type { UserComment } from '../../../../../models/admin/userDetail/userActivity';

export default function CommentsActivity() {
const { myCommentsData, isLoading } = useGetMyComments();
const { userId } = useParams();

const { userActivityData, isLoading } = useGetUserActivity(
Number(userId),
'comments'
);

if (isLoading) {
return (
Expand All @@ -16,21 +24,27 @@ export default function CommentsActivity() {
);
}

if (!myCommentsData || myCommentsData.length === 0) {
if (
!userActivityData ||
!Array.isArray(userActivityData) ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 데이터 없어도 [] 빈배열로 들어오지않나요? 그러면 이거는 항상 true 조건 아닌가요? 그래서 length로 했던것같은데

userActivityData.length === 0
) {
return (
<S.WrapperNoContentAppliedProjects data-type='noContent'>
<NoContent type='comment' />
</S.WrapperNoContentAppliedProjects>
);
}

const commentsData = userActivityData as MyComments[] | UserComment[];

return (
<S.Container>
<S.CommentsWrapper>
{myCommentsData.map((list, idx: number) => (
{commentsData.map((list: MyComments | UserComment, idx: number) => (
<Fragment key={list.id}>
<CommentActivity list={list} />
{idx !== myCommentsData.length - 1 && (
{idx !== commentsData.length - 1 && (
<S.CommentBorder></S.CommentBorder>
)}
</Fragment>
Expand Down
14 changes: 11 additions & 3 deletions src/components/user/mypage/activityLog/inquiries/Inquiries.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { useGetMyInquiries } from '../../../../../hooks/user/useGetMyInquiries';
import { useParams } from 'react-router-dom';
import useGetUserActivity from '../../../../../hooks/admin/useGetAllUserActivity';
import ContentBorder from '../../../../common/contentBorder/ContentBorder';
import NoContent from '../../../../common/noContent/NoContent';
import Spinner from '../../Spinner';
import * as S from './Inquiries.styled';
import Inquiry from './inquiry/Inquiry';
import type { MyInquiries } from '../../../../../models/activityLog';

export default function Inquiries() {
const { myInquiriesData, isLoading } = useGetMyInquiries();
const { userId } = useParams();
const { userActivityData, isLoading } = useGetUserActivity(
Number(userId),
'inquiries'
);
Comment on lines +11 to +15
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

URL 파라미터 검증 누락

userId 파라미터가 유효한지 검증하지 않고 바로 Number(userId)로 변환하고 있습니다. 잘못된 파라미터가 전달될 경우 문제가 발생할 수 있습니다.

export default function Inquiries() {
  const { userId } = useParams();
+ 
+ if (!userId || isNaN(Number(userId))) {
+   return (
+     <S.WrapperNoContentAppliedProjects data-type='noContent'>
+       <NoContent type='error' />
+     </S.WrapperNoContentAppliedProjects>
+   );
+ }
+ 
  const { userActivityData, isLoading } = useGetUserActivity(
    Number(userId),
    'inquiries'
  );
📝 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 { userId } = useParams();
const { userActivityData, isLoading } = useGetUserActivity(
Number(userId),
'inquiries'
);
export default function Inquiries() {
const { userId } = useParams();
if (!userId || isNaN(Number(userId))) {
return (
<S.WrapperNoContentAppliedProjects data-type='noContent'>
<NoContent type='error' />
</S.WrapperNoContentAppliedProjects>
);
}
const { userActivityData, isLoading } = useGetUserActivity(
Number(userId),
'inquiries'
);
// ...rest of the component
}
🤖 Prompt for AI Agents
In src/components/user/mypage/activityLog/inquiries/Inquiries.tsx around lines
12 to 16, the userId URL parameter is converted to a number without validation,
which can cause issues if the parameter is invalid. Add validation to check if
userId exists and is a valid number before converting it. Handle cases where
userId is missing or invalid by showing an error message or redirecting
appropriately to prevent runtime errors.


if (isLoading) {
return (
Expand All @@ -16,13 +22,15 @@ export default function Inquiries() {
);
}

if (!myInquiriesData || myInquiriesData?.length === 0)
if (!userActivityData || userActivityData?.length === 0)
return (
<S.WrapperNoContentAppliedProjects data-type='noContent'>
<NoContent type='inquiries' />
</S.WrapperNoContentAppliedProjects>
);

const myInquiriesData = userActivityData as MyInquiries[];

return (
<S.container>
<S.InquiriesContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import { useRef, useState } from 'react';
import type { MyInquiries } from '../../../../../../models/activityLog';
import * as S from './Inquiry.styled';
import { My_INQUIRIES_MESSAGE } from '../../../../../../constants/user/customerService';
Expand Down
2 changes: 1 addition & 1 deletion src/components/user/mypage/myProfile/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ROUTES } from '../../../../../constants/routes';
import 'chart.js/auto';
import { chartOptions } from '../../../../../constants/evaluationChartData';
import { formatDate } from '../../../../../util/formatDate';
import { UserInfoAll } from '../../../../../models/userInfo';
import type { UserInfoAll } from '../../../../../models/userInfo';

export default function Profile() {
const {
Expand Down
Loading