Refactor: 컴포넌트 에러 등 및 캐싱 오류 디바운스 도입 등

This commit is contained in:
정현조 2024-09-23 11:19:24 +09:00
parent b2e886df45
commit 8add900d24
5 changed files with 99 additions and 41 deletions

View File

@ -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);

View File

@ -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>
);

View File

@ -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();

View File

@ -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 />

View File

@ -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) => (