Skip to content

Commit

Permalink
Refactor: #153 SearchUserInput 컴포넌트 디바운스, Abort 로직 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
Seok93 committed Sep 23, 2024
1 parent bc10423 commit 0752c35
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 40 deletions.
64 changes: 60 additions & 4 deletions src/components/common/SearchUserInput.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,85 @@
import { useCallback, useEffect, useRef } from 'react';
import { IoSearch } from 'react-icons/io5';
import { findUser } from '@services/userService';
import { findUserByTeam } from '@services/teamService';
import { findUserByProject } from '@services/projectService';

import type { SearchUser } from '@/types/UserType';

type FetchCallback<T> = T extends (...arg: infer P) => void ? (...arg: P) => Promise<void> : never;

type AllSearchCallback = {
type: 'ALL';
searchCallback: FetchCallback<typeof findUser>;
};

type TeamSearchCallback = {
type: 'TEAM';
searchCallback: FetchCallback<typeof findUserByTeam>;
};

type ProjectSearchCallback = {
type: 'PROJECT';
searchCallback: FetchCallback<typeof findUserByProject>;
};

type SearchCallback = AllSearchCallback | TeamSearchCallback | ProjectSearchCallback;

type SearchInputProps = {
id: string;
label: string;
keyword: string;
searchId: number;
loading: boolean;
userList: SearchUser[];
searchCallbackInfo: SearchCallback;
onKeywordChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSearchKeyup: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onSearchClick: () => void;
onUserClick: (user: SearchUser) => void;
};

export default function SearchUserInput({
id,
label,
searchId,
keyword,
loading,
userList,
searchCallbackInfo,
onKeywordChange: handleKeywordChange,
onSearchKeyup: handleSearchKeyUp,
onSearchClick: handleSearchClick,
onUserClick: handleUserClick,
}: SearchInputProps) {
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);

const searchUsers = useCallback(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (abortControllerRef.current) abortControllerRef.current.abort();

abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;

const { type, searchCallback } = searchCallbackInfo;

if (type === 'ALL') searchCallback(keyword, { signal });
else searchCallback(searchId, keyword, { signal });
}, [searchCallbackInfo, searchId, keyword]);

useEffect(() => {
if (keyword) debounceRef.current = setTimeout(() => searchUsers(), 500);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (abortControllerRef.current) abortControllerRef.current.abort();
};
}, [searchUsers, keyword]);

const handleSearchClick = () => searchUsers();
const handleSearchKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key.toLowerCase() === 'enter') {
e.preventDefault();
searchUsers();
}
};

return (
<label htmlFor={id} className="group mb-10 flex items-center gap-5">
<h3 className="text-large">{label}</h3>
Expand Down
52 changes: 16 additions & 36 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { DateTime } from 'luxon';
import { FormProvider, useForm } from 'react-hook-form';

Expand Down Expand Up @@ -36,8 +36,6 @@ type ModalTaskFormProps = {
// ToDo: React Query Error시 처리 추가할 것
export default function ModalTaskForm({ formId, project, taskId, onSubmit }: ModalTaskFormProps) {
const { projectId, startDate, endDate } = project;
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);

const [keyword, setKeyword] = useState('');
const [assignees, setAssignees] = useState<UserWithRole[]>([]);
Expand All @@ -49,7 +47,16 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
const { data: userList = [], loading, clearData, fetchData } = useAxios(findUserByProject);
const { toastInfo, toastWarn } = useToast();

// ToDo: 상태 수정 모달 작성시 기본값 설정 방식 변경할 것
type FetchCallback<T> = T extends (...arg: infer P) => void ? (...arg: P) => Promise<void> : never;
type ProjectSearchCallback = {
type: 'PROJECT';
searchCallback: FetchCallback<typeof findUserByProject>;
};
const searchInfo: ProjectSearchCallback = useMemo(
() => ({ type: 'PROJECT', searchCallback: fetchData }),
[fetchData],
);

const methods = useForm<TaskForm>({
mode: 'onChange',
defaultValues: {
Expand All @@ -62,6 +69,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
files: [],
},
});

const {
register,
watch,
Expand All @@ -70,42 +78,15 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
formState: { errors },
} = methods;

const searchUsers = useCallback(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (abortControllerRef.current) abortControllerRef.current.abort();

abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;

fetchData(projectId, keyword, { signal });
}, [fetchData, projectId, keyword]);

useEffect(() => {
if (!isStatusLoading && statusList) {
setValue('statusId', statusList[0].statusId);
}
}, [isStatusLoading, statusList]);

useEffect(() => {
if (keyword) debounceRef.current = setTimeout(() => searchUsers(), 500);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (abortControllerRef.current) abortControllerRef.current.abort();
};
}, [searchUsers, keyword]);

const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => setKeyword(e.target.value.trim());

const handleSearchClick = () => searchUsers();

const handleSearchKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key.toLowerCase() === 'enter') {
e.preventDefault();
searchUsers();
}
};

const handleUserClick = (user: SearchUser) => {
const handlAssigneeClick = (user: SearchUser) => {
const isIncludedUser = assignees.find((assignee) => assignee.userId === user.userId);
if (isIncludedUser) return toastInfo('이미 포함된 수행자입니다');

Expand Down Expand Up @@ -196,18 +177,17 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
endDateFieldName="endDate"
/>

{/* ToDo: 검색UI 공용 컴포넌트로 추출할 것 */}
<div className="mb-20">
<SearchUserInput
id="search"
label="수행자"
keyword={keyword}
searchId={projectId}
loading={loading}
userList={userList}
searchCallbackInfo={searchInfo}
onKeywordChange={handleKeywordChange}
onSearchKeyup={handleSearchKeyUp}
onSearchClick={handleSearchClick}
onUserClick={handleUserClick}
onUserClick={handlAssigneeClick}
/>
<AssigneeList assigneeList={assignees} onAssigneeDeleteClick={handleAssigneeDeleteClick} />
</div>
Expand Down

0 comments on commit 0752c35

Please sign in to comment.