Skip to content

Commit

Permalink
Feat: #101 프로젝트 상태 생성 API를 이용하여 상태 생성 기능 구현 (#103)
Browse files Browse the repository at this point in the history
* Chore: #101 프로젝트 상태 타입 정의 수정

* Chore: #101 프로젝트 상태 타입 수정으로 인한 코드 수정

* Feat: #101 프로젝트 상태 생성 API를 위한 React Query 기능 추가

* Feat: #101 프로젝트 상태 생성 API MSW 기능 추가

* Feat: #101 프로젝트 상태 생성 기능 추가

* Chore: #101 불필요한 import 삭제

* Fix: #101 초기값 변수 이름 수정
  • Loading branch information
Seok93 authored Sep 4, 2024
1 parent 3967b28 commit 96ff8a7
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import ModalFormButton from '@components/modal/ModalFormButton';
import type { SubmitHandler } from 'react-hook-form';
import type { Project } from '@/types/ProjectType';
import type { ProjectStatusForm } from '@/types/ProjectStatusType';
import { useCreateStatus } from '@/hooks/query/useStatusQuery';

type CreateModalProjectStatusProps = {
project: Project;
onClose: () => void;
};

export default function CreateModalProjectStatus({ project, onClose: handleClose }: CreateModalProjectStatusProps) {
// ToDo: 상태 생성을 위한 네트워크 로직 추가
const statusMutation = useCreateStatus(project.projectId);

// ToDo: Error 처리 추가할 것
const handleSubmit: SubmitHandler<ProjectStatusForm> = async (data) => {
console.log('생성 폼 제출');
console.log(data);
statusMutation.mutate(data);
statusMutation.reset();
handleClose();
};
return (
Expand Down
16 changes: 11 additions & 5 deletions src/components/modal/project-status/ModalProjectStatusForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { RiProhibited2Fill } from 'react-icons/ri';
import { STATUS_VALIDATION_RULES } from '@constants/formValidationRules';
import Spinner from '@components/common/Spinner';
import DuplicationCheckInput from '@components/common/DuplicationCheckInput';
import useStatusQuery from '@hooks/query/useStatusQuery';
import { useReadStatuses } from '@hooks/query/useStatusQuery';

import type { SubmitHandler } from 'react-hook-form';
import { useEffect } from 'react';
import type { ProjectStatus, ProjectStatusForm } from '@/types/ProjectStatusType';
import type { Project } from '@/types/ProjectType';

Expand All @@ -17,21 +18,26 @@ type ModalProjectStatusFormProps = {
};

export default function ModalProjectStatusForm({ formId, project, statusId, onSubmit }: ModalProjectStatusFormProps) {
const { isStatusLoading, initialValue, nameList, colorList, usableColorList } = useStatusQuery(
const { isStatusLoading, initialValue, nameList, colorList, usableColorList } = useReadStatuses(
project.projectId,
statusId,
);

const {
register,
watch,
reset,
handleSubmit,
formState: { errors },
} = useForm<ProjectStatusForm>({
mode: 'onChange',
defaultValues: initialValue,
});

useEffect(() => {
reset(initialValue);
}, [initialValue, reset]);

if (isStatusLoading) {
return (
<section className="flex grow items-center justify-center">
Expand All @@ -45,10 +51,10 @@ export default function ModalProjectStatusForm({ formId, project, statusId, onSu
<DuplicationCheckInput
id="name"
label="상태명"
value={watch('name')}
value={watch('statusName')}
placeholder="상태명을 입력하세요."
errors={errors.name?.message}
register={register('name', STATUS_VALIDATION_RULES.STATUS_NAME(nameList))}
errors={errors.statusName?.message}
register={register('statusName', STATUS_VALIDATION_RULES.STATUS_NAME(nameList))}
/>
<h3 className="text-large">색상</h3>
<section className="grid grid-cols-8 gap-4">
Expand Down
12 changes: 6 additions & 6 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import DuplicationCheckInput from '@components/common/DuplicationCheckInput';
import useToast from '@hooks/useToast';
import useAxios from '@hooks/useAxios';
import { useTasksQuery } from '@hooks/query/useTaskQuery';
import useStatusQuery from '@hooks/query/useStatusQuery';
import { useReadStatuses } from '@hooks/query/useStatusQuery';
import { convertBytesToString } from '@utils/converter';
import { findUserByProject } from '@services/projectService';

Expand Down Expand Up @@ -44,7 +44,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
const [preview, setPreview] = useState(false);
const [files, setFiles] = useState<CustomFile[]>([]);

const { statusList, isStatusLoading } = useStatusQuery(projectId, taskId);
const { statusList, isStatusLoading } = useReadStatuses(projectId, taskId);
const { taskNameList } = useTasksQuery(projectId);
const { data, loading, clearData, fetchData } = useAxios(findUserByProject);
const { toastInfo, toastWarn } = useToast();
Expand Down Expand Up @@ -183,24 +183,24 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
{/* ToDo: 상태 선택 리팩토링 할 것 */}
<div className="flex items-center justify-start gap-4">
{statusList.map((status) => {
const { statusId, name, colorCode } = status;
const { statusId, statusName, colorCode } = status;
const isChecked = +watch('statusId') === statusId;
return (
<label
key={statusId}
htmlFor={name}
htmlFor={statusName}
className={`flex cursor-pointer items-center rounded-lg border px-5 py-3 text-emphasis ${isChecked ? 'border-input bg-white' : 'bg-button'}`}
>
<input
id={name}
id={statusName}
type="radio"
className="invisible h-0 w-0"
value={statusId}
checked={isChecked}
{...register('statusId', TASK_VALIDATION_RULES.STATUS)}
/>
<div style={{ borderColor: colorCode }} className="mr-3 h-8 w-8 rounded-full border" />
<h3 className="text-xs">{name}</h3>
<h3 className="text-xs">{statusName}</h3>
</label>
);
})}
Expand Down
4 changes: 2 additions & 2 deletions src/components/task/kanban/ProjectStatusContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type TaskStatusContainerProps = {
export default function TaskStatusContainer({ statusTask }: TaskStatusContainerProps) {
const { project } = useProjectContext();
const { showModal, openModal, closeModal } = useModal();
const { statusId, name, colorCode, sortOrder, tasks } = statusTask;
const { statusId, statusName, colorCode, sortOrder, tasks } = statusTask;
const draggableId = useMemo(() => generatePrefixId(statusId, DND_DRAGGABLE_PREFIX.STATUS), [statusId]);
const index = useMemo(() => sortOrder - 1, [sortOrder]);

Expand All @@ -30,7 +30,7 @@ export default function TaskStatusContainer({ statusTask }: TaskStatusContainerP
{...statusDragProvided.draggableProps}
>
<header className="flex items-center gap-4" {...statusDragProvided.dragHandleProps}>
<h2 className="select-none font-bold text-emphasis">{name}</h2>
<h2 className="select-none font-bold text-emphasis">{statusName}</h2>
<span>
<BsPencil
className="cursor-pointer hover:scale-110 hover:text-main hover:duration-150"
Expand Down
53 changes: 42 additions & 11 deletions src/hooks/query/useStatusQuery.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useMemo } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import useToast from '@hooks/useToast';
import { PROJECT_STATUS_COLORS } from '@constants/projectStatus';
import { useQuery } from '@tanstack/react-query';
import { getStatusList } from '@services/statusService';
import { createStatus, getStatusList } from '@services/statusService';

import type { Project } from '@/types/ProjectType';
import type { ProjectStatus, UsableColor } from '@/types/ProjectStatusType';
import type { ProjectStatus, ProjectStatusForm, UsableColor } from '@/types/ProjectStatusType';

function getStatusNameList(statusList: ProjectStatus[], excludedName?: ProjectStatus['name']) {
const statusNameList = statusList.map((projectStatus) => projectStatus.name);
function getStatusNameList(statusList: ProjectStatus[], excludedName?: ProjectStatus['statusName']) {
const statusNameList = statusList.map((projectStatus) => projectStatus.statusName);

const statusNameSet = new Set(statusNameList);
if (excludedName && statusNameSet.has(excludedName)) {
Expand Down Expand Up @@ -46,7 +49,8 @@ function getUsableStatusColorList(
}

// ToDo: ProjectStatus 관련 Query 로직 작성하기
export default function useStatusQuery(projectId: Project['projectId'], statusId?: ProjectStatus['statusId']) {
// ToDo: React Query 로직과 initialValue, nameList 등을 구하는 로직이 사실 관련이 없는 것 같음. 분리 고려하기.
export function useReadStatuses(projectId: Project['projectId'], statusId?: ProjectStatus['statusId']) {
const {
data: statusList = [],
isLoading: isStatusLoading,
Expand All @@ -60,11 +64,21 @@ export default function useStatusQuery(projectId: Project['projectId'], statusId
},
});

const status = statusList.find((status) => status.statusId === statusId);
const initialValue = { name: status?.name || '', color: status?.colorCode || '' };
const nameList = getStatusNameList(statusList, status?.name);
const colorList = getStatusColorList(statusList, status?.colorCode);
const usableColorList = getUsableStatusColorList(statusList, status?.colorCode);
const status = useMemo(() => statusList.find((status) => status.statusId === statusId), [statusList, statusId]);
const initialValue = useMemo(
() => ({
statusName: status?.statusName || '',
colorCode: status?.colorCode || '',
sortOrder: status?.sortOrder || statusList.length,
}),
[status],
);
const nameList = useMemo(() => getStatusNameList(statusList, status?.statusName), [statusList, status?.statusName]);
const colorList = useMemo(() => getStatusColorList(statusList, status?.colorCode), [statusList, status?.colorCode]);
const usableColorList = useMemo(
() => getUsableStatusColorList(statusList, status?.colorCode),
[statusList, status?.colorCode],
);

return {
statusList,
Expand All @@ -77,3 +91,20 @@ export default function useStatusQuery(projectId: Project['projectId'], statusId
usableColorList,
};
}

export function useCreateStatus(projectId: Project['projectId']) {
const { toastSuccess } = useToast();
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: (formData: ProjectStatusForm) => createStatus(projectId, formData),
onSuccess: () => {
toastSuccess('프로젝트 상태를 추가하였습니다.');
queryClient.invalidateQueries({
queryKey: ['projects', projectId, 'statuses'],
});
},
});

return mutation;
}
26 changes: 13 additions & 13 deletions src/mocks/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,72 +485,72 @@ export const STATUS_DUMMY: ProjectStatus[] = [
{
statusId: 1,
projectId: 1,
name: '할일',
statusName: '할일',
colorCode: PROJECT_STATUS_COLORS.RED,
sortOrder: 1,
},
{
statusId: 2,
projectId: 1,
name: '진행중',
statusName: '진행중',
colorCode: PROJECT_STATUS_COLORS.YELLOW,
sortOrder: 2,
},
{
statusId: 3,
projectId: 1,
name: '완료',
statusName: '완료',
colorCode: PROJECT_STATUS_COLORS.GREEN,
sortOrder: 3,
},
// 프로젝트2 상태
{
statusId: 4,
projectId: 2,
name: '할일',
statusName: '할일',
colorCode: PROJECT_STATUS_COLORS.RED,
sortOrder: 1,
},
{
statusId: 5,
projectId: 2,
name: '진행중',
statusName: '진행중',
colorCode: PROJECT_STATUS_COLORS.YELLOW,
sortOrder: 2,
},
{
statusId: 6,
projectId: 2,
name: '완료',
statusName: '완료',
colorCode: PROJECT_STATUS_COLORS.GREEN,
sortOrder: 3,
},
// 프로젝트3 상태
{
statusId: 7,
projectId: 3,
name: 'To Do',
statusName: 'To Do',
colorCode: PROJECT_STATUS_COLORS.YELLOW,
sortOrder: 1,
},
{
statusId: 8,
projectId: 3,
name: 'In Progress',
statusName: 'In Progress',
colorCode: PROJECT_STATUS_COLORS.ORANGE,
sortOrder: 2,
},
{
statusId: 9,
projectId: 3,
name: 'In Review',
statusName: 'In Review',
colorCode: PROJECT_STATUS_COLORS.RED,
sortOrder: 3,
},
{
statusId: 10,
projectId: 3,
name: 'Done',
statusName: 'Done',
colorCode: PROJECT_STATUS_COLORS.BLUE,
sortOrder: 4,
},
Expand Down Expand Up @@ -654,7 +654,7 @@ export const TASK_DUMMY: Task[] = [
export const TASK_SPECIAL_DUMMY: TaskListWithStatus[] = [
{
statusId: 1,
name: 'To Do',
statusName: 'To Do',
colorCode: '#c83c00',
sortOrder: 1,
tasks: [
Expand Down Expand Up @@ -695,7 +695,7 @@ export const TASK_SPECIAL_DUMMY: TaskListWithStatus[] = [
},
{
statusId: 2,
name: 'In Progress',
statusName: 'In Progress',
colorCode: '#dab700',
sortOrder: 2,
tasks: [
Expand Down Expand Up @@ -725,7 +725,7 @@ export const TASK_SPECIAL_DUMMY: TaskListWithStatus[] = [
},
{
statusId: 3,
name: 'Done',
statusName: 'Done',
colorCode: '#237700',
sortOrder: 3,
tasks: [
Expand Down
14 changes: 14 additions & 0 deletions src/mocks/services/statusServiceHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { http, HttpResponse } from 'msw';
import { STATUS_DUMMY } from '@mocks/mockData';

import type { ProjectStatusForm } from '@/types/ProjectStatusType';

const BASE_URL = import.meta.env.VITE_BASE_URL;

const statusServiceHandler = [
Expand All @@ -15,6 +17,18 @@ const statusServiceHandler = [

return HttpResponse.json(statusList);
}),
// 프로젝트 상태 생성 API
http.post(`${BASE_URL}/project/:projectId/status`, async ({ request, params }) => {
const accessToken = request.headers.get('Authorization');
const { projectId } = params;
const formData = (await request.json()) as ProjectStatusForm;

if (!accessToken) return new HttpResponse(null, { status: 401 });

STATUS_DUMMY.push({ statusId: STATUS_DUMMY.length, projectId: Number(projectId), ...formData });

return new HttpResponse(null, { status: 200 });
}),
];

export default statusServiceHandler;
2 changes: 1 addition & 1 deletion src/pages/project/CalendarPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function getCalendarTask(statusTasks: TaskListWithStatus[]) {
const calendarTasks: TaskWithStatus[] = [];

statusTasks.forEach((statusTask) => {
const { name: statusName, colorCode, sortOrder: statusOrder, tasks } = statusTask;
const { statusName, colorCode, sortOrder: statusOrder, tasks } = statusTask;
tasks.forEach((task) => {
calendarTasks.push({ statusName, colorCode, statusOrder, ...task });
});
Expand Down
Loading

0 comments on commit 96ff8a7

Please sign in to comment.