Refactor: 컴포넌트 에러 등 및 캐싱 오류 디바운스 도입 등
This commit is contained in:
parent
b2e886df45
commit
8add900d24
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@ -7,6 +7,7 @@ import { Button } from '../ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import SearchInput from '../ui/search-input';
|
||||
import useSearchMembersByEmailQuery from '@/queries/members/useSearchMembersByEmailQuery';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
type PrivilegeType = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
|
||||
@ -38,10 +39,22 @@ export default function MemberAddForm({ onSubmit }: { onSubmit: (data: MemberAdd
|
||||
});
|
||||
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const { data: members } = useSearchMembersByEmailQuery(keyword);
|
||||
const [debouncedKeyword, setDebouncedKeyword] = useState(keyword);
|
||||
const { data: members } = useSearchMembersByEmailQuery(debouncedKeyword);
|
||||
|
||||
const [selectedMemberId, setSelectedMemberId] = useState<number | null>(null);
|
||||
|
||||
const handleKeywordChange = debounce((value: string) => {
|
||||
setDebouncedKeyword(value);
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
handleKeywordChange(keyword);
|
||||
return () => {
|
||||
handleKeywordChange.cancel();
|
||||
};
|
||||
}, [handleKeywordChange, keyword]);
|
||||
|
||||
const handleMemberSelect = (memberId: number) => {
|
||||
form.setValue('memberId', memberId);
|
||||
setSelectedMemberId(memberId);
|
||||
|
@ -3,18 +3,34 @@ import MemberAddForm, { MemberAddFormValues } from './MemberAddForm';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
import useAddProjectMemberQuery from '@/queries/projects/useAddProjectMemberQuery';
|
||||
import { ProjectMemberRequest } from '@/types';
|
||||
|
||||
interface MemberAddModalProps {
|
||||
onSubmit: (data: MemberAddFormValues) => void;
|
||||
projectId: number;
|
||||
buttonClass?: string;
|
||||
}
|
||||
|
||||
export default function MemberAddModal({ onSubmit, buttonClass = '' }: MemberAddModalProps) {
|
||||
export default function MemberAddModal({ projectId, buttonClass = '' }: MemberAddModalProps) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const { mutate: addProjectMember } = useAddProjectMemberQuery();
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
const handleMemberAdd = (data: MemberAddFormValues) => {
|
||||
const newMember: ProjectMemberRequest = {
|
||||
memberId: data.memberId,
|
||||
privilegeType: data.role,
|
||||
};
|
||||
addProjectMember({
|
||||
projectId,
|
||||
memberId: data.memberId,
|
||||
newMember,
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
@ -27,17 +43,12 @@ export default function MemberAddModal({ onSubmit, buttonClass = '' }: MemberAdd
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<Plus size={16} />
|
||||
<span>멤버 초대하기</span>
|
||||
<span>프로젝트 멤버 초대하기</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader title="새 멤버 초대" />
|
||||
<MemberAddForm
|
||||
onSubmit={(data: MemberAddFormValues) => {
|
||||
onSubmit(data);
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
<MemberAddForm onSubmit={handleMemberAdd} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -36,8 +36,7 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje
|
||||
onSubmit={(data: ProjectCreateFormValues) => {
|
||||
const formattedData: ProjectRequest = {
|
||||
title: data.projectName,
|
||||
projectType: (data.labelType.charAt(0).toUpperCase() +
|
||||
data.labelType.slice(1)) as ProjectRequest['projectType'],
|
||||
projectType: data.labelType.toLowerCase() as ProjectRequest['projectType'],
|
||||
};
|
||||
onSubmit(formattedData);
|
||||
handleClose();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@ -6,6 +6,7 @@ import { Form, FormControl, FormItem, FormLabel, FormMessage } from '../ui/form'
|
||||
import { Button } from '../ui/button';
|
||||
import SearchInput from '../ui/search-input';
|
||||
import useSearchMembersByEmailQuery from '@/queries/members/useSearchMembersByEmailQuery';
|
||||
import debounce from 'lodash/debounce'; // 디바운스 사용
|
||||
|
||||
const formSchema = z.object({
|
||||
memberId: z.number().nonnegative({ message: '멤버를 선택하세요.' }),
|
||||
@ -24,7 +25,19 @@ export default function MemberAddForm({ onSubmit }: { onSubmit: (data: MemberAdd
|
||||
});
|
||||
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const { data: members } = useSearchMembersByEmailQuery(keyword);
|
||||
const [debouncedKeyword, setDebouncedKeyword] = useState(keyword);
|
||||
const { data: members } = useSearchMembersByEmailQuery(debouncedKeyword);
|
||||
|
||||
const handleKeywordChange = debounce((value: string) => {
|
||||
setDebouncedKeyword(value);
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
handleKeywordChange(keyword);
|
||||
return () => {
|
||||
handleKeywordChange.cancel();
|
||||
};
|
||||
}, [handleKeywordChange, keyword]);
|
||||
|
||||
const handleMemberSelect = (memberId: number) => {
|
||||
form.setValue('memberId', memberId);
|
||||
@ -42,7 +55,7 @@ export default function MemberAddForm({ onSubmit }: { onSubmit: (data: MemberAdd
|
||||
<SearchInput
|
||||
placeholder="초대할 멤버의 이메일을 검색하세요."
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onChange={(e) => setKeyword(e.target.value)} // 사용자 입력에 따라 상태 업데이트
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
@ -6,9 +6,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery';
|
||||
import useRemoveProjectMemberQuery from '@/queries/projects/useRemoveProjectMemberQuery';
|
||||
import useAddProjectMemberQuery from '@/queries/projects/useAddProjectMemberQuery';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import MemberAddModal from '@/components/MemberAddModal';
|
||||
type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER' | 'NONE';
|
||||
const roles: Role[] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER', 'NONE'];
|
||||
const roleToStr: { [key in Role]: string } = {
|
||||
@ -26,50 +26,72 @@ export default function ProjectMemberManage() {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const previousProjectId = useRef(projectId);
|
||||
|
||||
const { data: projectMembers = [] } = useProjectMembersQuery(Number(projectId), memberId);
|
||||
const { data: workspaceMembers = [] } = useWorkspaceMembersQuery(Number(workspaceId));
|
||||
|
||||
const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery();
|
||||
const { mutate: removeMember } = useRemoveProjectMemberQuery();
|
||||
const { mutate: addProjectMember } = useAddProjectMemberQuery();
|
||||
const updatePrivilege = useUpdateProjectMemberPrivilegeQuery();
|
||||
const removeMember = useRemoveProjectMemberQuery();
|
||||
const addProjectMember = useAddProjectMemberQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||
if (projectId && previousProjectId.current !== projectId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', Number(previousProjectId.current), memberId] }); // 이전 projectId의 캐시 무효화
|
||||
queryClient.invalidateQueries({ queryKey: ['workspaceMembers', Number(workspaceId)] }); // workspaceMembers 무효화
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', Number(projectId), memberId] });
|
||||
|
||||
previousProjectId.current = projectId;
|
||||
}
|
||||
}, [projectId, queryClient]);
|
||||
}, [projectId, workspaceId, memberId, queryClient]);
|
||||
|
||||
const noRoleMembers = workspaceMembers
|
||||
.filter((workspaceMember) => !projectMembers.some((projectMember) => projectMember.memberId === workspaceMember.id))
|
||||
.map((member) => ({
|
||||
memberId: member.id,
|
||||
nickname: member.nickname,
|
||||
profileImage: member.profileImage,
|
||||
privilegeType: 'NONE',
|
||||
}));
|
||||
const sortedMembers = useMemo(() => {
|
||||
const noRoleMembers = workspaceMembers
|
||||
.filter(
|
||||
(workspaceMember) => !projectMembers.some((projectMember) => projectMember.memberId === workspaceMember.id)
|
||||
)
|
||||
.map((member) => ({
|
||||
memberId: member.id,
|
||||
nickname: member.nickname,
|
||||
profileImage: member.profileImage,
|
||||
privilegeType: 'NONE',
|
||||
}));
|
||||
|
||||
const sortedMembers = [...projectMembers, ...noRoleMembers].sort((a, b) => {
|
||||
const aPrivilege = a.privilegeType || 'NONE';
|
||||
const bPrivilege = b.privilegeType || 'NONE';
|
||||
return roles.indexOf(aPrivilege as Role) - roles.indexOf(bPrivilege as Role);
|
||||
});
|
||||
return [...projectMembers, ...noRoleMembers].sort((a, b) => {
|
||||
const aPrivilege = a.privilegeType || 'NONE';
|
||||
const bPrivilege = b.privilegeType || 'NONE';
|
||||
return roles.indexOf(aPrivilege as Role) - roles.indexOf(bPrivilege as Role);
|
||||
});
|
||||
}, [projectMembers, workspaceMembers]);
|
||||
|
||||
const handleRoleChange = (memberId: number, role: Role) => {
|
||||
if (role === 'NONE') {
|
||||
removeMember({ projectId: Number(projectId), targetMemberId: memberId });
|
||||
removeMember.mutate({ projectId: Number(projectId), targetMemberId: memberId });
|
||||
} else {
|
||||
if (projectMembers.some((m) => m.memberId === memberId)) {
|
||||
updatePrivilege({ projectId: Number(projectId), memberId, privilegeData: { memberId, privilegeType: role } });
|
||||
updatePrivilege.mutate({
|
||||
projectId: Number(projectId),
|
||||
memberId,
|
||||
privilegeData: { memberId, privilegeType: role },
|
||||
});
|
||||
} else {
|
||||
addProjectMember({ projectId: Number(projectId), memberId, newMember: { memberId, privilegeType: role } });
|
||||
addProjectMember.mutate({
|
||||
projectId: Number(projectId),
|
||||
memberId,
|
||||
newMember: { memberId, privilegeType: role },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
|
||||
<div
|
||||
key={projectId}
|
||||
className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6"
|
||||
>
|
||||
<header className="flex w-full items-center gap-4">
|
||||
<h1 className="flex-1 text-lg font-semibold text-[#333238]">프로젝트 멤버 관리</h1>
|
||||
<MemberAddModal projectId={projectId ? Number(projectId) : 0} />
|
||||
</header>
|
||||
|
||||
{sortedMembers.map((member) => (
|
||||
|
Loading…
Reference in New Issue
Block a user