-
Notifications
You must be signed in to change notification settings - Fork 0
관리자 "회원 전체 조회" 페이지 구현 ( issue #330 ) #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
17c0a5a
efb41d8
8d80351
bad5752
27d61e7
c0ba886
e82178e
bc1addd
616fd08
9b8156f
2c552db
4af6e42
890fe34
f03682a
e2781b4
8a6b4dc
76912bd
671e812
37c542f
2818825
7d6e27d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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; | ||
| `; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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%; | ||
| `; | ||
| 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'; | ||||||||||||||||||||
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| 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} /> | ||||||||||||||||||||
| ))} | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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가 있다면 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| </S.SkillTagArea> | ||||||||||||||||||||
| <S.TextLabel>계정 생성 날짜</S.TextLabel> | ||||||||||||||||||||
| <S.TextContent>{userData.createdAt}</S.TextContent> | ||||||||||||||||||||
| </S.MainContentArea> | ||||||||||||||||||||
| </S.Container> | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export default UserCard; | ||||||||||||||||||||
| 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; | ||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. css reset 돼있지않니? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain@YouD0313, CSS reset 설정을 확인해보겠습니다. 🏁 Script executed: Length of output: 16004 @YouD0313, 맞습니다! 확인해보니 프로젝트에서 이미 sanitize.css가 적용되어 있고, // src/style/global.ts
import 'sanitize.css';
export const GlobalStyle = createGlobalStyle`
* {
margin: 0;
padding:0;
// ...
}그리고 현재 구현이 적절해 보입니다. 🙂 ✏️ Learnings added
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||
| 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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 적절한 도메인의 상수를 사용해주세요. 검색바 컴포넌트에서 고객 서비스 관련 상수( 관리자 검색바 전용 상수를 만들어주세요: // 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 |
||
| 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(); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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} | ||
| /> | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| {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> | ||
| ); | ||
| } | ||
| 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'; | ||
|
||
|
|
||
| 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 }; | ||
|
|
||
| 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 }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type