From 1925c3af9333f69ad27647e0490aecdd09df9859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=ED=98=84=ED=98=B8?= Date: Tue, 10 Sep 2024 14:01:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(member,=20time):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20(#232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .changeset/tender-tables-compete.md | 6 + apps/member/package.json | 1 + apps/member/src/api/member.ts | 13 ++ .../ManageLevelSection/AddMemberForm.tsx | 143 +++++++++++++++ .../ManageLevelSection/ManageLevelSection.tsx | 170 ++++-------------- .../ManageLevelSection/RoleEditView.tsx | 143 +++++++++++++++ apps/member/src/constants/api.ts | 3 +- apps/member/src/constants/select.ts | 20 ++- .../queries/member/useMemberAddMutation.ts | 37 ++++ apps/member/src/types/manage.ts | 22 +++ .../shared/hooks/useEditableSearchParams.ts | 2 +- apps/time/src/shared/ui/Modal.tsx | 2 +- .../time-table/ui/TimeTableLectureTable.tsx | 20 ++- .../widgets/time-table/ui/TimeTableModal.tsx | 5 +- pnpm-lock.yaml | 54 ++---- 15 files changed, 443 insertions(+), 198 deletions(-) create mode 100644 .changeset/tender-tables-compete.md create mode 100644 apps/member/src/components/manage/ManageLevelSection/AddMemberForm.tsx create mode 100644 apps/member/src/components/manage/ManageLevelSection/RoleEditView.tsx create mode 100644 apps/member/src/hooks/queries/member/useMemberAddMutation.ts create mode 100644 apps/member/src/types/manage.ts diff --git a/.changeset/tender-tables-compete.md b/.changeset/tender-tables-compete.md new file mode 100644 index 00000000..0f175ea6 --- /dev/null +++ b/.changeset/tender-tables-compete.md @@ -0,0 +1,6 @@ +--- +"@clab-platforms/member": minor +"@clab-platforms/time": minor +--- + +feat(member, time): 관리자 페이지 계정 추가 기능 diff --git a/apps/member/package.json b/apps/member/package.json index 4e21e4e9..eb0af108 100644 --- a/apps/member/package.json +++ b/apps/member/package.json @@ -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", diff --git a/apps/member/src/api/member.ts b/apps/member/src/api/member.ts index 9f20598b..b447dc96 100644 --- a/apps/member/src/api/member.ts +++ b/apps/member/src/api/member.ts @@ -7,6 +7,7 @@ import type { ResponsePagination, WithPaginationParams, } from '@type/api'; +import { AddMemberRequestType } from '@type/manage.ts'; import type { MemberInfo, MemberProfileRequestType, @@ -136,3 +137,15 @@ export async function patchMemberRole({ return data; } + +/** + * 멤버 추가 + */ +export async function addMember(body: AddMemberRequestType) { + const { data } = await server.post< + AddMemberRequestType, + BaseResponse + >({ url: END_POINT.MEMBER_ADD, body }); + + return data; +} diff --git a/apps/member/src/components/manage/ManageLevelSection/AddMemberForm.tsx b/apps/member/src/components/manage/ManageLevelSection/AddMemberForm.tsx new file mode 100644 index 00000000..aff0c13f --- /dev/null +++ b/apps/member/src/components/manage/ManageLevelSection/AddMemberForm.tsx @@ -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(defaultMemberInfo); + const { memberAddMutation } = useMemberAddMutation(); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + memberAddMutation(userInput); + }; + + const handleInputChange = ( + e: ChangeEvent, + ) => { + setUserInput((prev) => ({ + ...prev, + [e.target.name]: + e.target.name !== 'grade' + ? e.target.value + : SELECT_OPTIONS.GRADE[+e.target.value - 1].value, + })); + }; + + return ( +
+ + + + + + + + + setSearchWords(e.target.value)} - /> - handleSearchClick()} - /> - - - {data.items.map(({ id, name, role }, index) => ( - - {index + 1 + page * size} - {id} - {name} - - - {toKoreaMemberLevel(role)} - - - -
-
- + + + + } + > + {renderView} + ); diff --git a/apps/member/src/components/manage/ManageLevelSection/RoleEditView.tsx b/apps/member/src/components/manage/ManageLevelSection/RoleEditView.tsx new file mode 100644 index 00000000..5bb53790 --- /dev/null +++ b/apps/member/src/components/manage/ManageLevelSection/RoleEditView.tsx @@ -0,0 +1,143 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { + Badge, + BadgeColorVariant, + Button, + Input, + Table, +} from '@clab-platforms/design-system'; +import { SearchOutline } from '@clab-platforms/icon'; + +import Pagination from '@components/common/Pagination/Pagination.tsx'; +import Select from '@components/common/Select/Select.tsx'; + +import { TABLE_HEAD, TABLE_HEAD_ACTION } from '@constants/head.ts'; +import { ROLE_LEVEL } from '@constants/state.ts'; +import { usePagination } from '@hooks/common/usePagination.ts'; +import useToast from '@hooks/common/useToast.ts'; +import { useMemberRole, useMemberRoleMutation } from '@hooks/queries/member'; +import { isNumeric } from '@utils/member.ts'; +import { toKoreaMemberLevel } from '@utils/string.ts'; + +import { Role } from '@type/manage.ts'; +import type { RoleLevelKey } from '@type/member.ts'; + +interface RoleEditViewProps { + role: Role; +} + +const roleOptions = Object.keys(ROLE_LEVEL).map((key) => ({ + name: toKoreaMemberLevel(key as RoleLevelKey), + value: key as RoleLevelKey, +})); + +const roleColors: Record = { + SUPER: 'red', + ADMIN: 'blue', + USER: 'green', +}; + +const RoleEditView = ({ role }: RoleEditViewProps) => { + const navigation = useNavigate(); + const toast = useToast(); + const { page, size, handlePageChange } = usePagination({ + defaultSize: 6, + sectionName: 'level', + }); + const [searchWords, setSearchWords] = useState(''); + 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: role || undefined, + }); + + const { memberRoleMutation } = useMemberRoleMutation(); + + const handleSearchClick = () => { + refetch(); + }; + const handleLevelChangeButtonClick = (memberId: string) => { + if (!changeRole) { + return toast({ + state: 'error', + message: '권한을 선택해주세요', + }); + } else { + memberRoleMutation({ + memberId: memberId, + body: { role: changeRole }, + }); + } + }; + + useEffect(() => { + navigation('/manage'); + setSearchWords(''); + }, [role, navigation]); + + return ( + <> +
+ setSearchWords(e.target.value)} + /> + handleSearchClick()} + /> +
+ + {data.items.map(({ id, name, role }, index) => ( + + {index + 1 + page * size} + {id} + {name} + + {toKoreaMemberLevel(role)} + + +
+
+ + + ); +}; + +export default RoleEditView; diff --git a/apps/member/src/constants/api.ts b/apps/member/src/constants/api.ts index d4972949..ed63b2c2 100644 --- a/apps/member/src/constants/api.ts +++ b/apps/member/src/constants/api.ts @@ -20,9 +20,10 @@ export const END_POINT = { MY_NOTIFICATION: '/v1/notifications', MY_COMMENTS: '/v1/comments/my-comments', MY_INFO_EDIT: (id: string) => `/v1/members/${id}`, - // 멤버 래밸 관리 + // 멤버 관리 MEMBER_LEVEL: `/v1/members/roles`, MEMBER_LEVEL_EDIT: (memberId: string) => `/v1/members/${memberId}/roles`, + MEMBER_ADD: `v1/members`, // -- 커뮤니티 BOARDS: `/v1/boards`, BOARDS_LIST: `/v1/boards/category`, diff --git a/apps/member/src/constants/select.ts b/apps/member/src/constants/select.ts index f4d88d62..541bd0a3 100644 --- a/apps/member/src/constants/select.ts +++ b/apps/member/src/constants/select.ts @@ -8,13 +8,13 @@ type Options = { value: V; }; -type SelectOptions = { - [key: string]: Options[]; -}; +// type SelectOptions = { +// [key: string]: Options[]; +// }; export const SELECT_DEFAULT_OPTION = 'none'; -export const SELECT_OPTIONS: SelectOptions = { +export const SELECT_OPTIONS = { ACCOUNT_PANEL: [ { name: '1', value: '1시간' }, { name: '2', value: '2시간' }, @@ -31,6 +31,18 @@ export const SELECT_OPTIONS: SelectOptions = { { name: 'VR/AR', value: 'VR/AR' }, { name: 'Game', value: 'Game' }, ], + STUDENT_STATUS: [ + { name: '재학생', value: 'CURRENT' }, + { name: '휴학생', value: 'ON_LEAVE' }, + { name: '졸업생', value: 'GRADUATED' }, + ], + GRADE: [ + { name: '1학년', value: 1 }, + { name: '2학년', value: 2 }, + { name: '3학년', value: 3 }, + { name: '4학년', value: 4 }, + { name: '5학년', value: 5 }, + ], } as const; export const SELECT_OPTIONS_COMMUNITY_TYPE: Options< diff --git a/apps/member/src/hooks/queries/member/useMemberAddMutation.ts b/apps/member/src/hooks/queries/member/useMemberAddMutation.ts new file mode 100644 index 00000000..0b6ee0b9 --- /dev/null +++ b/apps/member/src/hooks/queries/member/useMemberAddMutation.ts @@ -0,0 +1,37 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { addMember } from '@api/member'; +import { MEMBER_QUERY_KEY } from '@constants/key'; +import useToast from '@hooks/common/useToast'; + +import { AddMemberRequestType } from '@type/manage.ts'; + +/** + * 멤버를 추가합니다. + */ +export const useMemberAddMutation = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + const mutation = useMutation({ + mutationFn: (body: AddMemberRequestType) => addMember(body), + onSuccess: (data) => { + if (data) { + queryClient.invalidateQueries({ + queryKey: MEMBER_QUERY_KEY.PAGES(), + }); + toast({ + state: 'success', + message: '계정이 생성되었어요.', + }); + } else { + toast({ + state: 'error', + message: '계정 생성에 실패했어요.', + }); + } + }, + }); + + return { memberAddMutation: mutation.mutate }; +}; diff --git a/apps/member/src/types/manage.ts b/apps/member/src/types/manage.ts new file mode 100644 index 00000000..81046c66 --- /dev/null +++ b/apps/member/src/types/manage.ts @@ -0,0 +1,22 @@ +/* +manage 섹션에서 사용되는 타입입니다. + */ +export type Role = '' | 'ADMIN' | 'USER' | 'SUPER'; + +export type Mode = 'view' | 'add'; + +export interface AddMemberRequestType { + id: string; + password: string; + name: string; + email: string; + contact: string; + department: string; + grade: number; + birth: string; + address: string; + interests: string; + githubUrl?: string; + studentStatus: string; + imageUrl?: string; +} diff --git a/apps/time/src/shared/hooks/useEditableSearchParams.ts b/apps/time/src/shared/hooks/useEditableSearchParams.ts index 60201c22..e58c768c 100644 --- a/apps/time/src/shared/hooks/useEditableSearchParams.ts +++ b/apps/time/src/shared/hooks/useEditableSearchParams.ts @@ -4,7 +4,7 @@ import { useSearchParams } from 'next/navigation'; function useEditableSearchParams() { const currentSearchParams = useSearchParams(); - const searchParams = new URLSearchParams(currentSearchParams); + const searchParams = new URLSearchParams(currentSearchParams.toString()); return searchParams; } diff --git a/apps/time/src/shared/ui/Modal.tsx b/apps/time/src/shared/ui/Modal.tsx index 547e9b5f..b4d4c719 100644 --- a/apps/time/src/shared/ui/Modal.tsx +++ b/apps/time/src/shared/ui/Modal.tsx @@ -198,7 +198,7 @@ export default function Modal({ title, close, size, children }: ModalProps) {
diff --git a/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx b/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx index 45c19a98..6e6ee7fe 100644 --- a/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx +++ b/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx @@ -27,15 +27,15 @@ interface TimeTableLectureTableItemProps { } const LECTURE_TABLE_ROW_HEADER = [ - { title: '캠퍼스', size: 1 }, - { title: '카테고리', size: 1 }, - { title: '과목코드', size: 1 }, - { title: '학점', size: 1 }, - { title: '학년', size: 1 }, + { title: '캠퍼스', size: 2 }, + { title: '카테고리', size: 2 }, + { title: '과목코드', size: 2 }, + { title: '학점', size: 2 }, + { title: '학년', size: 2 }, { title: '전공', size: 3 }, { title: '수업명', size: 4 }, { title: '담당교수', size: 3 }, - { title: '학기', size: 1 }, + { title: '학기', size: 2 }, { title: '시간', size: 3 }, { title: '수업구분', size: 7 }, ] as const; @@ -45,7 +45,7 @@ function TimeTableLectureNotification({ text }: { text: string }) { {text} @@ -118,7 +118,7 @@ function TimeTableLectureContent({ return ( - {data && ( + {data ? ( <> {data.length ? ( <> @@ -134,6 +134,8 @@ function TimeTableLectureContent({ )} + ) : ( + )} {hasNextPage && ( @@ -163,7 +165,7 @@ function TimeTableLectureTable({ {title} diff --git a/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx b/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx index a6cb2839..68807a16 100644 --- a/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx +++ b/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx @@ -259,11 +259,11 @@ const TimeTableModalMajorInput = memo(function TimeTableModalMajorInput({
setOpen(true)} > {selectedMajor && selectedValue} -
+