Skip to content

Commit

Permalink
feat(member, time): 관리자 페이지 계정 추가 기능 (#232)
Browse files Browse the repository at this point in the history
* refactor(member): minify layout shifting at `TimeTableModal`

* refactor(member): save `major` data in `TimeTableModal`

* refactor(member): normalize lecture remove modal size

* feat(member): implementing additional member request logic

* chore(member): install `@suspensive/react`

* feat(member): implementing member add-ons

* refactor(member): type `SelectOptions` temporarily disabled for intellisense

* Create tender-tables-compete.md

* refactor(member): change `invalidateQueries` queryKey when member addition success

* refactor(member): change certain keys to optional at `AddMemberRequestType`

* refactor(member): remove unnecessary query key

* refactor(member): reduce states by change states to object

* fix(time): fix type error at `useEditableSearchParams`

---------

Co-authored-by: GwanSik Kim <[email protected]>
  • Loading branch information
SWARVY and gwansikk authored Sep 10, 2024
1 parent 4342f54 commit 1925c3a
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 198 deletions.
6 changes: 6 additions & 0 deletions .changeset/tender-tables-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clab-platforms/member": minor
"@clab-platforms/time": minor
---

feat(member, time): 관리자 페이지 계정 추가 기능
1 change: 1 addition & 0 deletions apps/member/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@gwansikk/server-chain": "^0.5.2",
"@sentry/react": "^8.9.2",
"@sentry/vite-plugin": "^2.18.0",
"@suspensive/react": "^2.14.0",
"@tanstack/react-query": "^5.45.1",
"@tanstack/react-query-devtools": "^5.40.1",
"dayjs": "^1.11.10",
Expand Down
13 changes: 13 additions & 0 deletions apps/member/src/api/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ResponsePagination,
WithPaginationParams,
} from '@type/api';
import { AddMemberRequestType } from '@type/manage.ts';
import type {
MemberInfo,
MemberProfileRequestType,
Expand Down Expand Up @@ -136,3 +137,15 @@ export async function patchMemberRole({

return data;
}

/**
* 멤버 추가
*/
export async function addMember(body: AddMemberRequestType) {
const { data } = await server.post<
AddMemberRequestType,
BaseResponse<string>
>({ url: END_POINT.MEMBER_ADD, body });

return data;
}
143 changes: 143 additions & 0 deletions apps/member/src/components/manage/ManageLevelSection/AddMemberForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { ChangeEvent, FormEvent, useState } from 'react';

import { Button, Input } from '@clab-platforms/design-system';

import Select from '@components/common/Select/Select.tsx';

import { SELECT_OPTIONS } from '@constants/select.ts';
import { useMemberAddMutation } from '@hooks/queries/member/useMemberAddMutation.ts';

import { AddMemberRequestType } from '@type/manage.ts';

const defaultMemberInfo: AddMemberRequestType = {
id: '',
password: '',
name: '',
address: '',
email: '',
contact: '',
department: '',
grade: 1,
birth: '',
interests: '',
githubUrl: '',
imageUrl: '',
studentStatus: 'CURRENT',
};

const AddMemberForm = () => {
const [userInput, setUserInput] =
useState<AddMemberRequestType>(defaultMemberInfo);
const { memberAddMutation } = useMemberAddMutation();

const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
memberAddMutation(userInput);
};

const handleInputChange = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
setUserInput((prev) => ({
...prev,
[e.target.name]:
e.target.name !== 'grade'
? e.target.value
: SELECT_OPTIONS.GRADE[+e.target.value - 1].value,
}));
};

return (
<form className="flex flex-col gap-y-2" onSubmit={onSubmit}>
<Input
label="학번"
id="학번"
name="id"
placeholder="학번을 입력해주세요."
onChange={handleInputChange}
required
/>
<Input
label="이름"
id="이름"
name="name"
placeholder="이름을 입력해주세요."
onChange={handleInputChange}
required
/>
<Input
label="전화번호"
id="전화번호"
name="contact"
type="tel"
placeholder="전화번호를 입력해주세요."
onChange={handleInputChange}
required
/>
<Input
label="이메일"
id="이메일"
name="email"
type="email"
placeholder="이메일을 입력해주세요."
onChange={handleInputChange}
required
/>
<Input
label="학과"
id="학과"
name="department"
placeholder="학과를 입력해주세요."
onChange={handleInputChange}
required
/>
<Select
label="구분"
options={SELECT_OPTIONS.GRADE}
name="studentStatus"
onChange={handleInputChange}
/>
<Input
label="생년월일"
id="생년월일"
name="birth"
type="date"
placeholder="생년월일을 입력해주세요."
onChange={handleInputChange}
required
/>
<Input
label="주소"
id="주소"
name="address"
placeholder="주소를 입력해주세요."
onChange={handleInputChange}
required
/>
<Select
label="분야"
options={SELECT_OPTIONS.MY_FIELD}
name="interests"
onChange={handleInputChange}
/>
<Input
label="깃허브 주소"
id="깃허브 주소"
name="githubUrl"
placeholder="깃허브 주소를 입력해주세요."
onChange={handleInputChange}
/>
<Select
label="구분"
options={SELECT_OPTIONS.STUDENT_STATUS}
name="studentStatus"
onChange={handleInputChange}
/>
<Button type="submit" className="mt-2 w-full">
멤버 추가하기
</Button>
</form>
);
};

export default AddMemberForm;
Original file line number Diff line number Diff line change
@@ -1,178 +1,78 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';

import {
Badge,
BadgeColorVariant,
Button,
Input,
Menubar,
Table,
} from '@clab-platforms/design-system';
import { SearchOutline } from '@clab-platforms/icon';
import { Menubar, Spinner } from '@clab-platforms/design-system';

import Pagination from '@components/common/Pagination/Pagination';
import { Section } from '@components/common/Section';
import Select from '@components/common/Select/Select';
import AddMemberForm from '@components/manage/ManageLevelSection/AddMemberForm.tsx';
import RoleEditView from '@components/manage/ManageLevelSection/RoleEditView.tsx';

import { TABLE_HEAD, TABLE_HEAD_ACTION } from '@constants/head';
import { ROLE_LEVEL } from '@constants/state';
import { usePagination } from '@hooks/common/usePagination';
import useToast from '@hooks/common/useToast';
import { useMemberRole, useMemberRoleMutation } from '@hooks/queries/member';
import { isNumeric } from '@utils/member';
import { toKoreaMemberLevel } from '@utils/string';
import { Suspense } from '@suspensive/react';

import type { RoleLevelKey } from '@type/member';

type Mode = '' | 'ADMIN' | 'USER' | 'SUPER';

const roleOptions = Object.keys(ROLE_LEVEL).map((key) => ({
name: toKoreaMemberLevel(key as RoleLevelKey),
value: key as RoleLevelKey,
}));

const roleColors: Record<RoleLevelKey, BadgeColorVariant> = {
SUPER: 'red',
ADMIN: 'blue',
USER: 'green',
};
import { Mode, Role } from '@type/manage.ts';

const ManageLevelSection = () => {
const navigation = useNavigate();
const toast = useToast();
const { page, size, handlePageChange } = usePagination({
defaultSize: 6,
sectionName: 'level',
});
const [mode, setMode] = useState<Mode>('');
const [searchWords, setSearchWords] = useState<string>('');
const [changeRole, setChangeRole] = useState(roleOptions[0].value);

const { data, refetch } = useMemberRole({
page: page,
size: size,
memberId: searchWords && isNumeric(searchWords) ? searchWords : undefined,
memberName:
searchWords && !isNumeric(searchWords) ? searchWords : undefined,
role: mode || undefined,
});

const { memberRoleMutation } = useMemberRoleMutation();
const [role, setRole] = useState<Role>('');
const [mode, setMode] = useState<Mode>('view');

const handleMenubarItemClick = (mode: Mode) => {
setMode(mode);
};
const handleSearchClick = () => {
refetch();
};
const handleLevelChangeButtonClick = (memberId: string) => {
if (!changeRole) {
return toast({
state: 'error',
message: '권한을 선택해주세요',
});
} else {
memberRoleMutation({
memberId: memberId,
body: { role: changeRole },
});
}
const handleMenubarItemClick = (role: Role) => {
setRole(role);
setMode('view');
};

useEffect(() => {
navigation('/manage');
setSearchWords('');
}, [mode, navigation]);
const renderView = {
view: <RoleEditView role={role} />,
add: <AddMemberForm />,
}[mode];

return (
<Section>
<Section.Header
title="멤버 관리"
description="멤버 목록을 조회하고 권한을 관리할 수 있어요"
description="멤버 목록과 권한을 관리할 수 있어요"
>
<Menubar>
<Menubar.Item
selected={mode === ''}
selected={role === '' && mode === 'view'}
onClick={() => handleMenubarItemClick('')}
>
전체
</Menubar.Item>
<Menubar.Item
selected={mode === 'SUPER'}
selected={role === 'SUPER' && mode === 'view'}
onClick={() => handleMenubarItemClick('SUPER')}
>
관리자
</Menubar.Item>
<Menubar.Item
selected={mode === 'ADMIN'}
selected={role === 'ADMIN' && mode === 'view'}
onClick={() => handleMenubarItemClick('ADMIN')}
>
운영진
</Menubar.Item>
<Menubar.Item
selected={mode === 'USER'}
selected={role === 'USER' && mode === 'view'}
onClick={() => handleMenubarItemClick('USER')}
>
일반
</Menubar.Item>
<Menubar.Item
selected={mode === 'add'}
onClick={() => setMode('add')}
>
추가
</Menubar.Item>
</Menubar>
</Section.Header>
<Section.Body>
<div className="mb-4 flex gap-2">
<Input
className="w-full"
id="searchWords"
name="searchWords"
value={searchWords}
placeholder="학번이나 이름을 입력해주세요"
onChange={(e) => setSearchWords(e.target.value)}
/>
<SearchOutline
width={24}
height={24}
className="m-auto hover:cursor-pointer"
onClick={() => handleSearchClick()}
/>
</div>
<Table head={[...TABLE_HEAD.MEMBER_MANAGE_TABLE, TABLE_HEAD_ACTION]}>
{data.items.map(({ id, name, role }, index) => (
<Table.Row key={id}>
<Table.Cell>{index + 1 + page * size}</Table.Cell>
<Table.Cell>{id}</Table.Cell>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>
<Badge color={roleColors[role]}>
{toKoreaMemberLevel(role)}
</Badge>
</Table.Cell>
<Table.Cell>
<div className="mx-auto flex items-center justify-center gap-2">
<Select
id="changeRole"
options={roleOptions}
onChange={(e) =>
setChangeRole(e.target.value as RoleLevelKey)
}
/>
<Button
size="sm"
onClick={() => handleLevelChangeButtonClick(id)}
>
변경하기
</Button>
</div>
</Table.Cell>
</Table.Row>
))}
</Table>
<Pagination
className="mt-4 justify-center"
page={page}
postLimit={size}
totalItems={data.totalItems}
onChange={handlePageChange}
/>
<Suspense
fallback={
<div className="pb-52 pt-40 text-center">
<Spinner />
</div>
}
>
{renderView}
</Suspense>
</Section.Body>
</Section>
);
Expand Down
Loading

0 comments on commit 1925c3a

Please sign in to comment.