Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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 { SearchType } from '../models/search';
Copy link
Collaborator

Choose a reason for hiding this comment

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

type


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
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 @@ -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,9 @@ 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: ${({ $isDone }) => ($isDone ? `#07DE00` : `#DE1A00`)};
`;

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
60 changes: 60 additions & 0 deletions src/components/admin/userCard/UserCard.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import styled from 'styled-components';

export const Container = styled.div`
width: 240px;
display: flex;
flex-direction: column;
border: 1px solid #000000;
Copy link
Collaborator

Choose a reason for hiding this comment

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

테마에 있는 색은 테마로 해주세욥ㅋㅋ

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?: '접속 중' | '오프라인' | '정지';
}>`
font-size: 14px;
color: ${({ $userState }) =>
$userState === '접속 중'
? `#39E81E`
: $userState === '오프라인'
? `#2560E8`
: $userState === '정지'
? `#E8000C`
: `#000000`};
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 #ccc;
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 { AllUser } from '../../../models/auth';
Copy link
Collaborator

Choose a reason for hiding this comment

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

type


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 src={skillTag.img} />
))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

React key prop이 누락되었습니다.

스킬 태그를 렌더링하는 map 함수에서 각 요소에 대한 key prop이 누락되었습니다. 이는 React의 효율적인 리렌더링을 방해할 수 있습니다.

다음 diff를 적용하여 수정하세요:

-          {userData.skill.map((skillTag) => (
-            <S.SkillTag src={skillTag.img} />
-          ))}
+          {userData.skill.map((skillTag, index) => (
+            <S.SkillTag key={`skill-${skillTag.id || index}`} src={skillTag.img} />
+          ))}

스킬 태그에 고유한 ID가 있다면 skillTag.id를 사용하고, 없다면 index를 fallback으로 사용하세요.

📝 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
{userData.skill.map((skillTag) => (
<S.SkillTag src={skillTag.img} />
))}
{userData.skill.map((skillTag, index) => (
<S.SkillTag
key={`skill-${skillTag.id || index}`}
src={skillTag.img}
/>
))}
🤖 Prompt for AI Agents
In src/components/admin/userCard/UserCard.tsx around lines 36 to 38, the map
rendering skill tags is missing the React key prop, which is necessary for
efficient re-rendering. Fix this by adding a key prop to the <S.SkillTag>
component, using skillTag.id if it exists, or the index from the map function as
a fallback.

</S.SkillTagArea>
<S.TextLabel>계정 생성 날짜</S.TextLabel>
<S.TextContent>{userData.createdAt}</S.TextContent>
</S.MainContentArea>
</S.Container>
);
};

export default UserCard;
40 changes: 40 additions & 0 deletions src/components/common/admin/searchBar/SearchBar.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import styled from 'styled-components';

export const AdminSearchBarContainer = styled.form`
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 2rem;
`;

export const AdminSearchBarWrapper = styled.div`
display: flex;
width: 70%;
justify-content: space-between;
padding: 0.5rem 0.5rem 0.5rem 1rem;
border: 1px solid ${({ theme }) => theme.color.deepGrey};
border-radius: ${({ theme }) => theme.borderRadius.large} 0 0
${({ theme }) => theme.borderRadius.large};
`;

export const AdminSearchBarInput = styled.input`
width: 100%;
font-size: 1.3rem;
`;
Copy link

@coderabbitai coderabbitai bot Jun 2, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 필드 스타일링 개선이 필요합니다.

입력 필드에 기본적인 스타일링이 누락되어 있습니다.

다음과 같은 스타일을 추가하는 것을 권장합니다:

export const AdminSearchBarInput = styled.input`
  width: 100%;
  font-size: 1.3rem;
+ border: none;
+ outline: none;
+ background: transparent;
`;
📝 Committable suggestion

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

Suggested change
export const AdminSearchBarInput = styled.input`
width: 100%;
font-size: 1.3rem;
`;
export const AdminSearchBarInput = styled.input`
width: 100%;
font-size: 1.3rem;
border: none;
outline: none;
background: transparent;
`;
🤖 Prompt for AI Agents
In src/components/common/admin/searchBar/SearchBar.styled.ts around lines 20 to
23, the AdminSearchBarInput styled component lacks basic input field styling.
Add essential styles such as padding, border, border-radius, and focus outline
to improve usability and appearance. Ensure the input field looks consistent and
accessible by including these common style properties.

Copy link
Collaborator

Choose a reason for hiding this comment

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

css reset 돼있지않니?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@YouD0313, CSS reset 설정을 확인해보겠습니다.


🏁 Script executed:

#!/bin/bash

# package.json에서 CSS reset 관련 패키지 확인
echo "=== package.json에서 reset/normalize CSS 패키지 확인 ==="
cat package.json | jq '.dependencies, .devDependencies' | grep -i -E "(reset|normalize|sanitize)"

echo -e "\n=== index.html에서 CSS reset 확인 ==="
fd -name "index.html" --exec cat {} \; | grep -i -E "(reset|normalize)"

echo -e "\n=== 전역 스타일 파일들 확인 ==="
fd -e css -e ts -e tsx | grep -i -E "(global|reset|normalize|index)" | head -10

echo -e "\n=== styled-components 전역 스타일 확인 ==="
rg -A 10 -B 5 "createGlobalStyle|GlobalStyle" 

echo -e "\n=== 기존 input 스타일링 사례 확인 ==="
rg -A 5 -B 2 "styled\.input|styled\(.*input" 

Length of output: 16004


@YouD0313, 맞습니다! 확인해보니 프로젝트에서 이미 sanitize.css가 적용되어 있고, src/style/global.ts에서 추가적인 CSS reset도 구현되어 있네요.

// src/style/global.ts
import 'sanitize.css';

export const GlobalStyle = createGlobalStyle`
  * {
    margin: 0;
    padding:0;
    // ...
  }

그리고 AdminSearchBarWrapper에서 이미 border를 처리하고 있어서 input 자체에는 추가적인 border 스타일이 필요하지 않습니다. 제가 불필요한 제안을 드렸네요. 죄송합니다!

현재 구현이 적절해 보입니다. 🙂


✏️ Learnings added
Learnt from: YouD0313
PR: devpalsPlus/frontend#330
File: src/components/common/admin/searchBar/SearchBar.styled.ts:20-23
Timestamp: 2025-06-03T02:14:03.997Z
Learning: devpalsPlus 프로젝트에서는 sanitize.css와 GlobalStyle을 통해 CSS reset이 적용되어 있어, input 요소의 기본 border와 outline 스타일이 이미 정규화되어 있다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


export const AdminSearchBarBackIcon = styled.button`
svg {
width: 1.5rem;
}
`;

export const AdminSearchBarButton = styled.button`
width: 10%;
border: 1px solid ${({ theme }) => theme.color.navy};
background: ${({ theme }) => theme.color.navy};
border-radius: 0 ${({ theme }) => theme.borderRadius.large}
${({ theme }) => theme.borderRadius.large} 0;
font-size: 1.3rem;
color: ${({ theme }) => theme.color.white};
padding: 0.5rem 1rem 0.5rem 0.5rem;
`;
75 changes: 75 additions & 0 deletions src/components/common/admin/searchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { XMarkIcon } from '@heroicons/react/24/outline';
import * as S from './SearchBar.styled';
import { useState } from 'react';
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';

interface SearchBarProps {
onGetKeyword: (value: string) => void;
value: string;
}

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

const handleKeyword = (inputValue: string) => {
const newSearchParams = new URLSearchParams(searchParams);
if (inputValue === '') {
newSearchParams.delete('keyword');
} else {
newSearchParams.set('keyword', inputValue);
}
setSearchParams(newSearchParams);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

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

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

const handleClickSearchDefault = () => {
setKeyword('');
onGetKeyword('');
handleKeyword('');
};

return (
<S.AdminSearchBarContainer onSubmit={handleSubmit}>
<S.AdminSearchBarWrapper>
<S.AdminSearchBarInput
placeholder={MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword}
value={keyword ? keyword : value}
onChange={handleChangeKeyword}
/>
{value && (
<S.AdminSearchBarBackIcon
type='button'
onClick={handleClickSearchDefault}
>
<XMarkIcon />
</S.AdminSearchBarBackIcon>
)}
</S.AdminSearchBarWrapper>
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
<Modal isOpen={isOpen} onClose={handleModalClose}>
{message}
</Modal>
</S.AdminSearchBarContainer>
);
}
7 changes: 4 additions & 3 deletions src/hooks/admin/useGetAllUsers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import { UserData } from '../queries/user/keys';
import { getAllUsers } from '../../api/auth.api';
import { SearchType } from '../../models/search';
Copy link
Collaborator

Choose a reason for hiding this comment

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

type


export const useGetAllUsers = () => {
export const useGetAllUsers = (searchUnit: SearchType) => {
const {
data: allUserData,
isLoading,
isFetching,
} = useQuery({
queryKey: [UserData.allUser],
queryFn: () => getAllUsers(),
queryKey: [UserData.allUser, searchUnit.keyword, searchUnit.page],
queryFn: () => getAllUsers(searchUnit),
});

return { allUserData, isLoading, isFetching };
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/admin/useGetAllUsersPreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { UserData } from '../queries/user/keys';
import { getAllUsersPreview } from '../../api/auth.api';

export const useGetAllUsersPreview = () => {
const {
data: allUserData,
isLoading,
isFetching,
} = useQuery({
queryKey: [UserData.allUserPreview],
queryFn: () => getAllUsersPreview(),
select: (allUsers) => allUsers.slice(0, 5),
});

return { allUserData, isLoading, isFetching };
};
Loading