Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
17c0a5a
feat : "미리보기"와 "전체 조회" API 구분
layout-SY Jun 2, 2025
efb41d8
feat : "회원 전체 조회 미리보기"에서 useQuery의 select 옵션을 통해 데이터의 상위 5개만 출력
layout-SY Jun 2, 2025
8d80351
feat : mock 통신 함수 "미리보기"와 "회원 전체 조회" 구분
layout-SY Jun 2, 2025
bad5752
refactor : 타입 네이밍 수정
layout-SY Jun 2, 2025
27d61e7
feat : "회원 전체 조회" mock 데이터 추가
layout-SY Jun 2, 2025
c0ba886
feat : "회원 전체 조회" 모델 추가
layout-SY Jun 2, 2025
e82178e
feat : "회원 전체 조회"의 회원 카드 리스트 구현
layout-SY Jun 2, 2025
bc1addd
style : "회원 전체 조회" 카드 스타일 수정
layout-SY Jun 2, 2025
616fd08
feat : 검색 및 페이지네이션에 맞게 모델 수정
layout-SY Jun 2, 2025
9b8156f
feat : SearchBar를 통해 검색 구현
layout-SY Jun 2, 2025
2c552db
feat : "회원 전체 조회" 페이지 구현
layout-SY Jun 2, 2025
4af6e42
feat : 변경된 API에 맞게 mock 데이터 수정
layout-SY Jun 2, 2025
890fe34
Merge branch 'develop' of https://github.com/devpalsPlus/frontend int…
layout-SY Jun 2, 2025
f03682a
Merge branch 'develop' of https://github.com/devpalsPlus/frontend int…
layout-SY Jun 5, 2025
e2781b4
feat : 리뷰 사항 적용
layout-SY Jun 5, 2025
8a6b4dc
feat : 색에 theme 적용
layout-SY Jun 5, 2025
76912bd
refactor : searchBar의 상태 및 코드 hook화
layout-SY Jun 5, 2025
671e812
feat : searchBar에 "작성하기" 버튼 분기 생성
layout-SY Jun 5, 2025
37c542f
feat : UserCard 테두리 색 변경
layout-SY Jun 5, 2025
2818825
feat : searchBar에서 검색 후 뒤로 가기 했을 때 전체 리스트 나오게 설정
layout-SY Jun 5, 2025
7d6e27d
feat : searchBar에 검색한 내용을 유지하기 위한 value 롤백
layout-SY Jun 5, 2025
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
18 changes: 16 additions & 2 deletions src/api/auth.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ApiGetAllUsers,
ApiGetAllUsersPreview,
type ApiOauth,
type ApiVerifyNickname,
type VerifyEmail,
Expand All @@ -8,6 +9,7 @@ import { httpClient } from './http.api';
import { loginFormValues } from '../pages/login/Login';
import { registerFormValues } from '../pages/user/register/Register';
import { changePasswordFormValues } from '../pages/user/changePassword/ChangePassword';
import { type SearchType } from '../models/search';

export const postVerificationEmail = async (email: string) => {
try {
Expand Down Expand Up @@ -106,9 +108,21 @@ export const getOauthLogin = async (oauthAccessToken: string) => {
}
};

export const getAllUsers = async () => {
export const getAllUsersPreview = async () => {
try {
const response = await httpClient.get<ApiGetAllUsers>(`/users`);
const response = await httpClient.get<ApiGetAllUsersPreview>(
`/users/preview`
);
return response.data.data;
} catch (e) {
console.error(e);
throw e;
}
};

export const getAllUsers = async (params: SearchType) => {
try {
const response = await httpClient.get<ApiGetAllUsers>(`/users`, { params });
return response.data.data;
} catch (e) {
console.error(e);
Expand Down
39 changes: 10 additions & 29 deletions src/components/admin/adminNotice/AdminNoticeList.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,15 @@
import { useEffect, useState } from 'react';
import SearchBar from '../../../components/common/admin/searchBar/SearchBar';
import * as S from './AdminNoticeList.styled';
import type { NoticeSearch } from '../../../models/customerService';
import { useGetNotice } from '../../../hooks/user/useGetNotice';
import { useSearchParams } from 'react-router-dom';
import Pagination from '../../../components/common/pagination/Pagination';
import Spinner from '../../../components/user/mypage/Spinner';
import NoticeItem from '../../../pages/user/customerService/notice/noticeItem/NoticeItem';
import useSearchBar from '../../../hooks/admin/useSearchBar';

export default function AdminNoticeList() {
const [noticeSearch, setNoticeSearch] = useState<NoticeSearch>({
keyword: '',
page: 1,
});
const [value, setValue] = useState<string>('');
const { noticeData, isLoading } = useGetNotice(noticeSearch);
const [searchParams] = useSearchParams();

useEffect(() => {
const searchKeyword = searchParams.get('keyword');

if (searchKeyword) {
setNoticeSearch((prev) => ({ ...prev, keyword: searchKeyword }));
setValue((prev) => (searchKeyword ? searchKeyword : prev));
}
}, [searchParams]);

const handleGetKeyword = (keyword: string) => {
setNoticeSearch((prev) => ({ ...prev, keyword }));
setValue(keyword);
};
const handleChangePagination = (page: number) => {
setNoticeSearch((prev) => ({ ...prev, page }));
};
const { searchUnit, value, handleGetKeyword, handleChangePagination } =
useSearchBar();
const { noticeData, isLoading } = useGetNotice(searchUnit);

if (isLoading) {
return (
Expand All @@ -48,7 +25,11 @@ export default function AdminNoticeList() {

return (
<>
<SearchBar onGetKeyword={handleGetKeyword} value={value} />
<SearchBar
onGetKeyword={handleGetKeyword}
value={value}
isNotice={true}
/>
<S.NoticeItemWrapper>
<NoticeItem
noticeData={noticeData.notices}
Expand All @@ -57,7 +38,7 @@ export default function AdminNoticeList() {
/>
</S.NoticeItemWrapper>
<Pagination
page={noticeSearch.page}
page={searchUnit.page}
getLastPage={lastPage}
onChangePagination={handleChangePagination}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/mainCard/MainCard.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from 'styled-components';
export const Container = styled.div`
display: flex;
flex-direction: column;
border: 1px solid #ccc;
border: 1px solid ${({ theme }) => theme.color.grey};
border-radius: ${({ theme }) => theme.borderRadius.primary};
`;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import * as S from './AllUserPreview.styled';
import { useGetAllUsers } from '../../../../hooks/admin/useGetAllUsers';
import Avatar from '../../../common/avatar/Avatar';
import { ADMIN_ROUTE } from '../../../../constants/routes';
import arrow_right from '../../../../assets/ArrowRight.svg';
import LoadingSpinner from '../../../common/loadingSpinner/LoadingSpinner';
import { useGetAllUsersPreview } from '../../../../hooks/admin/useGetAllUsersPreview';

const AllUserPreview = () => {
const { allUserData, isLoading, isFetching } = useGetAllUsers();
const { allUserData, isLoading, isFetching } = useGetAllUsersPreview();

if (isLoading || isFetching) {
return <LoadingSpinner />;
Expand All @@ -17,15 +17,9 @@ const AllUserPreview = () => {
return <S.Container>가입된 회원이 없습니다.</S.Container>;
}

const previewList = allUserData
? allUserData.length > 6
? allUserData.slice(0, 4)
: allUserData
: [];

return (
<S.Container>
{previewList?.map((user) => (
{allUserData?.map((user) => (
<S.Wrapper key={user.id}>
<S.UserArea>
<Avatar image={user.user.img} size='40px' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export const Divider = styled.p`

export const InquiryState = styled.p<{ $isCompleted: boolean }>`
font-size: 9px;
color: ${({ $isCompleted }) => ($isCompleted ? `#07DE00` : `#DE1A00`)};
color: ${({ theme, $isCompleted }) =>
$isCompleted ? theme.color.green : theme.color.red};
`;

export const MoveToInquiryArea = styled(Link)`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const ContentArea = styled.div`
margin-left: 16px;
`;

export const ImposedCount = styled.div`
export const ReportedCount = styled.div`
font-size: 9px;
opacity: 0.5;
`;
Expand Down Expand Up @@ -49,9 +49,10 @@ export const Divider = styled.p`
margin-right: 3px;
`;

export const IsImposed = styled.p<{ $isImposed: boolean }>`
export const IsDone = styled.p<{ $isDone: boolean }>`
font-size: 9px;
color: ${({ $isImposed }) => ($isImposed ? `#07DE00` : `#DE1A00`)};
color: ${({ theme, $isDone }) =>
$isDone ? theme.color.green : theme.color.red};
`;

export const MoveToReportsArea = styled(Link)`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ const ReportsPreview = () => {
<S.ReportArea to={`${ADMIN_ROUTE.reports}/${report.id}`}>
<Avatar image={report.user.img} size='40px' />
<S.ContentArea>
<S.ImposedCount>{report.imposedCount} 번</S.ImposedCount>
<S.ReportedCount>{report.reportedCount} 번</S.ReportedCount>
<S.Category>{report.category}</S.Category>
<S.StateArea>
<S.ReportDate>{report.createdAt}</S.ReportDate>
<S.Divider>|</S.Divider>
<S.IsImposed $isImposed={report.isImposed}>
{report.isImposed ? '검토 완료' : '검토 미완료'}
</S.IsImposed>
<S.IsDone $isDone={report.isDone}>
{report.isDone ? '검토 완료' : '검토 미완료'}
</S.IsDone>
</S.StateArea>
</S.ContentArea>
</S.ReportArea>
Expand Down
61 changes: 61 additions & 0 deletions src/components/admin/userCard/UserCard.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import styled from 'styled-components';
import { UserState } from '../../../models/auth';

export const Container = styled.div`
width: 240px;
display: flex;
flex-direction: column;
border: 1px solid ${({ theme }) => theme.color.grey};
border-radius: ${({ theme }) => theme.borderRadius.primary};
padding: 10px;
`;

export const ProfileHeader = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;

export const NickName = styled.p`
margin-top: 5px;
`;

export const MainContentArea = styled.div`
padding: 15px;
`;

export const TextLabel = styled.label`
display: inline-block;
font-size: 14px;
opacity: 0.3;
word-break: break-word;
white-space: pre-wrap;
`;

export const TextContent = styled.p<{
$userState?: UserState;
}>`
font-size: 14px;
color: ${({ theme, $userState }) =>
$userState === UserState.ONLINE
? theme.color.green
: $userState === UserState.OFFLINE
? theme.color.blue
: $userState === UserState.SUSPENDED
? theme.color.red
: theme.color.white};
margin-left: 15px;
`;

export const SkillTagArea = styled.div`
display: flex;
gap: 4px;
margin-left: 15px;
`;

export const SkillTag = styled.img`
width: 29px;
height: 29px;
border: 1px solid ${({ theme }) => theme.color.grey};
border-radius: 50%;
`;
47 changes: 47 additions & 0 deletions src/components/admin/userCard/UserCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import * as S from './UserCard.styled';
import Avatar from '../../common/avatar/Avatar';
import { type AllUser } from '../../../models/auth';

interface UserCardProps {
userData: AllUser;
}

const UserCard = ({ userData }: UserCardProps) => {
return (
<S.Container>
<S.ProfileHeader>
<Avatar image={userData.user.img} size='46px' />
<S.NickName>{userData.user.nickname}</S.NickName>
</S.ProfileHeader>
<S.MainContentArea>
<S.TextLabel>이메일</S.TextLabel>
<S.TextContent>{userData.email}</S.TextContent>
<S.TextLabel>회원 상태</S.TextLabel>
<S.TextContent $userState={userData.userState}>
{userData.userState}
</S.TextContent>
<S.TextLabel>경고 횟수</S.TextLabel>
<S.TextContent>
{userData.reportedCount === 0
? '없음'
: `${userData.reportedCount}번`}
</S.TextContent>
<S.TextLabel>포지션</S.TextLabel>
<S.TextContent>
{userData.position.map((position) => position.name).join(', ')}
</S.TextContent>
<S.TextLabel>대표 스킬</S.TextLabel>
<S.SkillTagArea>
{userData.skill.map((skillTag) => (
<S.SkillTag key={skillTag.id} src={skillTag.img} />
))}
</S.SkillTagArea>
<S.TextLabel>계정 생성 날짜</S.TextLabel>
<S.TextContent>{userData.createdAt}</S.TextContent>
</S.MainContentArea>
</S.Container>
);
};

export default UserCard;
36 changes: 20 additions & 16 deletions src/components/common/admin/searchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { XMarkIcon } from '@heroicons/react/24/outline';
import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService';
import * as S from './SearchBar.styled';
import { useState } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import { useModal } from '../../../../hooks/useModal';
import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

적절한 도메인의 상수를 사용해주세요.

검색바 컴포넌트에서 고객 서비스 관련 상수(MODAL_MESSAGE_CUSTOMER_SERVICE)를 사용하고 있습니다. 또한 placeholder와 에러 메시지가 동일한 것은 UX 관점에서 혼란스러울 수 있습니다.

관리자 검색바 전용 상수를 만들어주세요:

// constants/admin/searchBar.ts
export const ADMIN_SEARCH_BAR_MESSAGES = {
  PLACEHOLDER: '검색어를 입력해주세요',
  NO_KEYWORD_ERROR: '검색어를 입력해주세요',
} as const;

그리고 적용:

- import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService';
+ import { ADMIN_SEARCH_BAR_MESSAGES } from '../../../../constants/admin/searchBar';

- placeholder={MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword}
+ placeholder={ADMIN_SEARCH_BAR_MESSAGES.PLACEHOLDER}

- return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword);
+ return handleModalOpen(ADMIN_SEARCH_BAR_MESSAGES.NO_KEYWORD_ERROR);

Also applies to: 56-56

🤖 Prompt for AI Agents
In src/components/common/admin/searchBar/SearchBar.tsx at lines 6 and 56,
replace the imported constant MODAL_MESSAGE_CUSTOMER_SERVICE with a new
admin-specific constant. Create a new file constants/admin/searchBar.ts
exporting ADMIN_SEARCH_BAR_MESSAGES with distinct placeholder and error message
values as suggested. Then update the SearchBar component to use
ADMIN_SEARCH_BAR_MESSAGES.PLACEHOLDER for the input placeholder and
ADMIN_SEARCH_BAR_MESSAGES.NO_KEYWORD_ERROR for the error message to improve UX
clarity.

import Modal from '../../modal/Modal';
import { ADMIN_ROUTE } from '../../../../constants/routes';

interface SearchBarProps {
onGetKeyword: (keyword: string) => void;
onGetKeyword: (value: string) => void;
value: string;
isNotice?: boolean;
}

export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
const [inputValue, setInputValue] = useState<string>('');
export default function SearchBar({
onGetKeyword,
value,
isNotice,
}: SearchBarProps) {
const [keyword, setKeyword] = useState<string>(value);
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const keyword = inputValue ? inputValue : value;

const handleKeyword = (inputValue: string) => {
const newSearchParams = new URLSearchParams(searchParams);
Expand All @@ -32,22 +35,21 @@ export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (inputValue.trim() === '') {
if (keyword.trim() === '') {
return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword);
} else {
onGetKeyword(inputValue);
handleKeyword(inputValue);
onGetKeyword(keyword);
handleKeyword(keyword);
return;
}
};

const handleChangeKeyword = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setInputValue(value);
setKeyword(e.target.value);
};

const handleClickSearchDefault = () => {
setInputValue('');
setKeyword('');
onGetKeyword('');
handleKeyword('');
};
Expand All @@ -73,9 +75,11 @@ export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
</S.AdminSearchBarInputWrapper>
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
</S.AdminSearchBarWrapper>
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
작성하기
</S.WriteLink>
{isNotice && (
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
작성하기
</S.WriteLink>
)}
<Modal isOpen={isOpen} onClose={handleModalClose}>
{message}
</Modal>
Expand Down
1 change: 1 addition & 0 deletions src/constants/admin/adminModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const ADMIN_MODAL_MESSAGE = {
writeDeleteSuccess: '삭제되었습니다.',
writeDeleteFail: '삭제가 실패하였습니다.',
writeError: '알수없는 에러가 발생했습니다.',
NO_RESULT: '결과가 존재하지 않습니다.',
};
Loading