Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/GU-99/grow-up-fe into fe…
Browse files Browse the repository at this point in the history
…ature/#69-user-setting-ui
  • Loading branch information
Yoonyesol committed Aug 19, 2024
2 parents 7cf194c + b1b7a37 commit d158e4f
Show file tree
Hide file tree
Showing 19 changed files with 779 additions and 126 deletions.
6 changes: 5 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"pkief.material-icon-theme",
"equinusocio.vsc-material-theme",
"wix.vscode-import-cost",
"streetsidesoftware.code-spell-checker"
"streetsidesoftware.code-spell-checker",
"vitest.explorer",
"crystal-spider.jsdoc-generator",
"simonsiefke.svg-preview",
"bierner.jsdoc-markdown-highlighting"
]
}
121 changes: 102 additions & 19 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { DateTime } from 'luxon';
import { IoSearch } from 'react-icons/io5';
import { useForm } from 'react-hook-form';
import { IoSearch } from 'react-icons/io5';

import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';
import ToggleButton from '@components/common/ToggleButton';
import DuplicationCheckInput from '@components/common/DuplicationCheckInput';
import useAxios from '@hooks/useAxios';
import useTaskQuery from '@hooks/query/useTaskQuery';
import useStatusQuery from '@hooks/query/useStatusQuery';
import { findUserByProject } from '@services/projectService';

import type { SubmitHandler } from 'react-hook-form';
import type { User } from '@/types/UserType';
import type { Project } from '@/types/ProjectType';
import type { Task, TaskForm } from '@/types/TaskType';

Expand All @@ -20,9 +24,17 @@ type ModalTaskFormProps = {
};

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 [hasDeadline, setHasDeadline] = useState(false);
const { statusList } = useStatusQuery(project.projectId, taskId);
const { taskNameList } = useTaskQuery(project.projectId);
const [keyword, setKeyword] = useState('');
const [workers, setWorkers] = useState<User[]>([]);

const { statusList } = useStatusQuery(projectId, taskId);
const { taskNameList } = useTaskQuery(projectId);
const { data, loading, clearData, fetchData } = useAxios(findUserByProject);

// ToDo: 상태 수정 모달 작성시 기본값 설정 방식 변경할 것
const {
register,
Expand All @@ -43,12 +55,49 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
},
});

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 (keyword.trim()) {
debounceRef.current = setTimeout(() => searchUsers(), 500);
}
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (abortControllerRef.current) abortControllerRef.current.abort();
};
}, [searchUsers, keyword]);

const handleDeadlineToggle = () => {
setValue('endDate', getValues('startDate'));
clearErrors('endDate');
setHasDeadline((prev) => !prev);
};

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

const handleSearchClick = () => searchUsers();

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

const handleUserClick = (user: User) => {
setWorkers((prev) => [...prev, user]);
setKeyword('');
clearData();
};

return (
<form
id={formId}
Expand Down Expand Up @@ -99,7 +148,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
<input
type="date"
id="startDate"
{...register('startDate', TASK_VALIDATION_RULES.START_DATE(project.startDate, project.endDate))}
{...register('startDate', TASK_VALIDATION_RULES.START_DATE(startDate, endDate))}
/>
<div className={`my-5 h-10 grow text-xs text-error ${errors.startDate ? 'visible' : 'invisible'}`}>
{errors.startDate?.message}
Expand All @@ -117,7 +166,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
disabled={!hasDeadline}
{...register(
'endDate',
TASK_VALIDATION_RULES.END_DATE(hasDeadline, project.startDate, project.endDate, watch('startDate')),
TASK_VALIDATION_RULES.END_DATE(hasDeadline, startDate, endDate, watch('startDate')),
)}
/>
<div className={`my-5 h-10 grow text-xs text-error ${errors.endDate ? 'visible' : 'invisible'}`}>
Expand All @@ -126,19 +175,53 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
</label>
</div>

<label htmlFor="user" className="mb-20 flex items-center gap-5">
<h3 className="text-large">수행자</h3>
<div className="relative grow">
<input
type="text"
id="user"
className="h-25 w-full rounded-md border border-input pl-10 pr-25 text-regular placeholder:text-xs"
/>
<div className="absolute right-5 top-1/2 -translate-y-1/2">
<IoSearch className="size-15 text-emphasis" />
</div>
</div>
</label>
{/* ToDo: 검색UI 공용 컴포넌트로 추출할 것 */}
<div className="mb-20">
<label htmlFor="search" className="group mb-10 flex items-center gap-5">
<h3 className="text-large">수행자</h3>
<section className="relative grow">
<input
type="text"
id="search"
className="h-25 w-full rounded-md border border-input pl-10 pr-25 text-regular placeholder:text-xs"
value={keyword}
onChange={handleKeywordChange}
onKeyDown={handleSearchKeyUp}
placeholder="닉네임으로 검색해주세요."
/>
<button
type="button"
aria-label="search"
className="absolute right-5 top-1/2 -translate-y-1/2 cursor-pointer"
onClick={handleSearchClick}
>
<IoSearch className="size-15 text-emphasis hover:text-black" />
</button>
{keyword && !loading && (
<ul className="invisible absolute left-0 right-0 rounded-md border-2 bg-white group-focus-within:visible">
{data && data.length === 0 ? (
<div className="h-20 border px-10 leading-8">&apos;{keyword}&apos; 의 검색 결과가 없습니다.</div>
) : (
data?.map((user) => (
<li className="h-20 border" key={user.userId}>
<button
type="button"
className="h-full w-full px-10 text-left hover:bg-sub"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.blur();
handleUserClick(user);
}}
>
{user.nickname}
</button>
</li>
))
)}
</ul>
)}
</section>
</label>
</div>

<label htmlFor="content" className="mb-20">
<h3 className="text-large">내용</h3>
Expand Down
9 changes: 9 additions & 0 deletions src/constants/units.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
// ToDo: MEMORY_UNITS 으로 묶는거 고려해보기
export const KB = 1024;
export const MB = 1024 * KB;
export const GB = 1024 * MB;

// ToDo: DeepFreeze로 변경할 것, 상수 정의 컨벤션 적용할 것
export const fileSizeUnits = Object.freeze([
{ unit: 'GB', value: GB },
{ unit: 'MB', value: MB },
{ unit: 'KB', value: KB },
{ unit: 'B', value: 1 },
]);

// ToDo: TIME_UNITS 으로 묶는거 고려해보기
export const MILLISECOND = 1;
export const SECOND = 1000 * MILLISECOND;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
Empty file removed src/hooks/.gitkeep
Empty file.
61 changes: 61 additions & 0 deletions src/hooks/useAxios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import axios from 'axios';
import { useState, useCallback } from 'react';
import type { AxiosResponse } from 'axios';

type PromiseCallback<T, P extends unknown[]> = (...args: P) => Promise<AxiosResponse<T>>;

/**
* Axios API 함수를 처리하는 커스텀 훅
*
* @export
* @template T - AxiosResponse의 응답 데이터 타입
* @template {unknown[]} P - API 함수에 전달되는 매개변수의 가변인자 타입 배열
* @param {PromiseCallback<T, P>} fetchCallback - API 요청을 수행하는 함수
* @returns {{
* data: T | undefined; // API 요청의 응답 데이터
* error: Error | null; // API 요청 중 발생한 에러
* loading: boolean; // 데이터 로딩 중인지 여부
* fetchData: (...args: P) => Promise<void>; // API 요청을 호출하는 함수
* }}
* @example
* const { data, error, loading, fetchData } = useAxios(fetchCallback) // fetchCallback에서 타입을 반환한다면, 자동 타입 추론이 가능
* const { data, error, loading, fetchData } = useAxios<User[], Parameters<typeof fetchCallback>>(fetchCallback);
*/
export default function useAxios<T, P extends unknown[]>(fetchCallback: PromiseCallback<T, P>) {
const [data, setData] = useState<T>();
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);

const clearData = useCallback(() => {
setData(undefined);
setError(null);
setLoading(false);
}, []);

const fetchData = useCallback(
async (...params: P) => {
try {
setLoading(true);
const response = await fetchCallback(...params);
setData(response.data);
} catch (error: unknown) {
setError(error as Error);

if (!axios.isAxiosError(error)) return;

if (error.request) {
// ToDo: 네트워크 요청을 보냈지만 응답이 없는 경우 에러 처리
} else if (error.response) {
// ToDo: 요청후 응답을 받았지만 200 이외의 응답 코드인 경우 예외 처리
} else {
// ToDo: request 설정 오류
}
} finally {
setLoading(false);
}
},
[fetchCallback],
);

return { data, error, loading, clearData, fetchData };
}
2 changes: 1 addition & 1 deletion src/mocks/browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
import handlers from '@mocks/handlers';

export const worker = setupWorker(...handlers);
9 changes: 7 additions & 2 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
// ToDo: API 설계되면 엔드포인트 넣어서 설정할 것.
export const handlers = [];
import userServiceHandler from '@mocks/services/userServiceHandler';
import teamServiceHandler from '@mocks/services/teamServiceHandler';
import projectServiceHandler from '@mocks/services/projectServiceHandler';

const handlers = [...userServiceHandler, ...teamServiceHandler, ...projectServiceHandler];

export default handlers;
Loading

0 comments on commit d158e4f

Please sign in to comment.