Skip to content

Commit 6781b4a

Browse files
authored
Merge pull request #424 from kakao-tech-campus-3rd-step3/feature/member-management-ui#422
[FEAT] ๋™์•„๋ฆฌ์› ๊ด€๋ฆฌํŽ˜์ด์ง€ UI ๋ฐ ๋ฐ˜์‘ํ˜• (#422)
2 parents b906aa1 + c91f899 commit 6781b4a

File tree

11 files changed

+596
-3
lines changed

11 files changed

+596
-3
lines changed
Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,73 @@
1+
import { useParams } from 'react-router-dom';
2+
import { LoadingSpinner } from '@/shared/components/LoadingSpinner';
3+
import { MemberHeaderSection } from './components/MemberHeaderSection';
4+
import { MemberTableSection } from './components/MemberTableSection';
5+
import { useMemberFilter } from './hooks/useMemberFilter';
6+
import { useMemberMutations } from './hooks/useMemberMutations';
7+
import * as S from './index.styled';
8+
import type { Member } from './types/member';
9+
10+
// TODO: API ์—ฐ๋™ ์‹œ ์‚ญ์ œ
11+
const MOCK_MEMBERS: Member[] = Array.from({ length: 10 }, (_, i) => ({
12+
id: i + 1,
13+
name: `ํ™๊ธธ๋™${i + 1}`,
14+
generation: `${(i % 3) + 1}๊ธฐ`,
15+
department: '์˜์–ด์˜๋ฌธํ•™๊ณผ',
16+
phoneNumber: '010-1234-5678',
17+
role: ['ํšŒ์žฅ๋‹จ', '์šด์˜ํŒ€', '๋™์•„๋ฆฌ์›'][i % 3] as Member['role'],
18+
joinDate: new Date(2024, 0, i + 1).toISOString(),
19+
}));
20+
121
export const MemberManagementPage = () => {
2-
return <></>;
22+
const { clubId } = useParams<{ clubId: string }>();
23+
24+
// TODO: API ์—ฐ๋™
25+
// const { data: members = [], isLoading, error } = useQuery({
26+
// queryKey: ['members', clubId],
27+
// queryFn: () => fetchMembers(clubId!),
28+
// enabled: !!clubId,
29+
// });
30+
const members = MOCK_MEMBERS;
31+
const isLoading = false;
32+
const error = null;
33+
34+
const { searchText, sortBy, filteredMembers, setSearchText, setSortBy } =
35+
useMemberFilter(members);
36+
37+
const { handleRoleChange, handleDeleteMember } = useMemberMutations(clubId || '');
38+
39+
const handleAddMember = () => {
40+
// TODO: ๋‹จ๊ฑด ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์—ด๊ธฐ
41+
console.log('๋‹จ๊ฑด ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์—ด๊ธฐ');
42+
};
43+
44+
const handleBulkUpload = () => {
45+
// TODO: ์—‘์…€ ์—…๋กœ๋“œ ๋ชจ๋‹ฌ ์—ด๊ธฐ
46+
console.log('์—‘์…€ ์—…๋กœ๋“œ ๋ชจ๋‹ฌ ์—ด๊ธฐ');
47+
};
48+
49+
if (isLoading) return <LoadingSpinner />;
50+
if (error) return <div>์—๋Ÿฌ ๋ฐœ์ƒ</div>;
51+
52+
return (
53+
<S.Container>
54+
<S.ContentWrapper>
55+
<MemberHeaderSection
56+
clubName='์ธํ„ฐ์—‘์Šค'
57+
searchText={searchText}
58+
sortBy={sortBy}
59+
onSearchChange={setSearchText}
60+
onSortChange={setSortBy}
61+
onAddMember={handleAddMember}
62+
onBulkUpload={handleBulkUpload}
63+
/>
64+
65+
<MemberTableSection
66+
members={filteredMembers}
67+
onRoleChange={handleRoleChange}
68+
onDelete={handleDeleteMember}
69+
/>
70+
</S.ContentWrapper>
71+
</S.Container>
72+
);
373
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// TODO: API์—ฐ๋™ํ•  ๋•Œ ์ด๋ฆ„ ์„ธ๋ถ„ํ™”(๋ณ€๊ฒฝ)ํ•˜๊ณ  ํŒŒ์ผ ๋ถ„๋ฆฌ
2+
3+
import { apiInstance } from '@/app/api/initInstance';
4+
import { handleAxiosError } from '@/shared/utils/handleAxiosError';
5+
import type { Member, MemberRole } from '../types/member';
6+
7+
export const fetchMembers = async (clubId: string): Promise<Member[]> => {
8+
try {
9+
const { data } = await apiInstance.get(`/clubs/${clubId}/members`);
10+
return data;
11+
} catch (e: unknown) {
12+
return handleAxiosError(e, 'ํšŒ์› ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
13+
}
14+
};
15+
16+
export const updateMemberRole = async (
17+
clubId: string,
18+
memberId: number,
19+
role: MemberRole,
20+
): Promise<void> => {
21+
try {
22+
await apiInstance.patch(`/clubs/${clubId}/members/${memberId}/role`, { role });
23+
} catch (e: unknown) {
24+
return handleAxiosError(e, 'ํšŒ์› ์—ญํ•  ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
25+
}
26+
};
27+
28+
export const deleteMember = async (clubId: string, memberId: number): Promise<void> => {
29+
try {
30+
await apiInstance.delete(`/clubs/${clubId}/members/${memberId}`);
31+
} catch (e: unknown) {
32+
return handleAxiosError(e, 'ํšŒ์› ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
33+
}
34+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import styled from '@emotion/styled';
2+
3+
export const Header = styled.div(({ theme }) => ({
4+
display: 'flex',
5+
justifyContent: 'space-between',
6+
alignItems: 'center',
7+
padding: '1.5rem 2rem 2.5rem 0rem',
8+
backgroundColor: theme.colors.bg,
9+
gap: '1rem',
10+
11+
['@media (max-width: 1018px)']: {
12+
//๋™์•„๋ฆฌ๋ช…์ด 7๊ธ€์ž์ธ ๊ฒฝ์šฐ๊นŒ์ง€ ํ•ธ๋“ค
13+
flexDirection: 'column',
14+
alignItems: 'flex-start',
15+
},
16+
}));
17+
18+
export const LeftGroup = styled.div({
19+
display: 'flex',
20+
alignItems: 'center',
21+
gap: '0.75rem',
22+
});
23+
24+
export const RightGroup = styled.div({
25+
display: 'flex',
26+
alignItems: 'center',
27+
gap: '0.75rem',
28+
});
29+
30+
export const AddButton = styled.button(({ theme }) => ({
31+
fontSize: theme.font.size.sm,
32+
fontWeight: theme.font.weight.medium,
33+
color: theme.colors.primary,
34+
backgroundColor: theme.colors.bg,
35+
border: `1px solid ${theme.colors.primary}`,
36+
borderRadius: theme.radius.md,
37+
padding: '0.5rem 1rem',
38+
cursor: 'pointer',
39+
transition: 'background-color 0.2s',
40+
height: '36px',
41+
42+
'&:hover': {
43+
backgroundColor: theme.colors.primary100,
44+
},
45+
['@media (max-width: 638px)']: {
46+
display: 'none',
47+
},
48+
}));
49+
50+
// ํ”Œ๋Ÿฌ์Šค ์•„์ด์ฝ˜ ๋ฒ„ํŠผ (638px ์ดํ•˜์—๋งŒ ํ‘œ์‹œ)
51+
export const AddIconButton = styled.button(({ theme }) => ({
52+
display: 'none',
53+
54+
['@media (max-width: 638px)']: {
55+
display: 'flex',
56+
alignItems: 'center',
57+
justifyContent: 'center',
58+
width: '32px',
59+
height: '32px',
60+
cursor: 'pointer',
61+
color: theme.colors.gray500,
62+
fontSize: '24px',
63+
backgroundColor: 'transparent',
64+
border: 'none',
65+
padding: '0',
66+
transition: 'color 0.2s ease, transform 0.3s ease',
67+
68+
'&:hover': {
69+
color: theme.colors.gray700,
70+
transform: 'rotate(90deg)',
71+
},
72+
},
73+
}));
74+
75+
export const SearchInputWrapper = styled.div(({ theme }) => ({
76+
position: 'relative',
77+
display: 'flex',
78+
alignItems: 'center',
79+
width: '180px',
80+
height: '40px',
81+
border: `1px solid ${theme.colors.gray200}`,
82+
borderRadius: theme.radius.md,
83+
backgroundColor: theme.colors.bg,
84+
padding: '0 0.75rem',
85+
gap: '0.5rem',
86+
transition: 'border-color 0.2s',
87+
88+
'&:focus-within': {
89+
borderColor: theme.colors.primary,
90+
},
91+
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
92+
width: '50%',
93+
},
94+
}));
95+
96+
export const SearchIcon = styled.span(({ theme }) => ({
97+
display: 'flex',
98+
alignItems: 'center',
99+
color: theme.colors.gray500,
100+
fontSize: '1.2rem',
101+
flexShrink: 0,
102+
}));
103+
104+
export const SearchInput = styled.input(({ theme }) => ({
105+
flex: 1,
106+
border: 'none',
107+
outline: 'none',
108+
backgroundColor: 'transparent',
109+
fontSize: theme.font.size.base,
110+
color: theme.colors.textPrimary,
111+
112+
'&::placeholder': {
113+
color: theme.colors.gray500,
114+
},
115+
}));
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { FiSearch, FiPlus } from 'react-icons/fi';
2+
import { Dropdown } from '@/shared/components/Dropdown';
3+
import { Text } from '@/shared/components/Text';
4+
import * as S from './index.styled';
5+
import type { SortOption } from '../../types/member';
6+
7+
type Props = {
8+
clubName: string;
9+
searchText: string;
10+
sortBy: SortOption;
11+
onSearchChange: (text: string) => void;
12+
onSortChange: (sort: SortOption) => void;
13+
onAddMember: () => void;
14+
onBulkUpload: () => void;
15+
};
16+
17+
const SORT_OPTIONS: SortOption[] = ['์ด๋ฆ„์ˆœ', '๋“ฑ๋ก์ˆœ'];
18+
19+
export const MemberHeaderSection = ({
20+
clubName,
21+
searchText,
22+
sortBy,
23+
onSearchChange,
24+
onSortChange,
25+
onAddMember,
26+
onBulkUpload,
27+
}: Props) => {
28+
return (
29+
<S.Header>
30+
<S.LeftGroup>
31+
<Text size='xl' weight='medium'>
32+
{clubName} ๋™์•„๋ฆฌ์› ๋ช…๋‹จ
33+
</Text>
34+
<S.AddButton onClick={onAddMember}>๋™์•„๋ฆฌ์› ์ถ”๊ฐ€</S.AddButton>
35+
<S.AddButton onClick={onBulkUpload}>์—‘์…€๋กœ ์ผ๊ด„ ๋“ฑ๋ก</S.AddButton>
36+
<S.AddIconButton onClick={onAddMember}>
37+
<FiPlus />
38+
</S.AddIconButton>
39+
</S.LeftGroup>
40+
41+
<S.RightGroup>
42+
<S.SearchInputWrapper>
43+
<S.SearchIcon>
44+
<FiSearch />
45+
</S.SearchIcon>
46+
<S.SearchInput
47+
type='text'
48+
placeholder='๊ฒ€์ƒ‰'
49+
value={searchText}
50+
onChange={(e) => onSearchChange(e.target.value)}
51+
/>
52+
</S.SearchInputWrapper>
53+
54+
<Dropdown
55+
value={sortBy}
56+
options={SORT_OPTIONS}
57+
placeholder='์ •๋ ฌ ๊ธฐ์ค€'
58+
onSelect={onSortChange}
59+
/>
60+
</S.RightGroup>
61+
</S.Header>
62+
);
63+
};

0 commit comments

Comments
ย (0)