From 26e8d95aa687cc562b8bd58c2a870e7e547c5cea Mon Sep 17 00:00:00 2001 From: seungchanwoo Date: Wed, 23 Oct 2024 23:38:55 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20#200=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/project/CreateModalProject.tsx | 9 +- .../modal/project/ModalProjectForm.tsx | 165 +++++++++++------- src/hooks/query/useProjectQuery.ts | 25 ++- src/mocks/mockAPI.ts | 24 ++- src/mocks/services/projectServiceHandler.ts | 54 ++++++ src/services/projectService.ts | 20 ++- src/types/ProjectType.tsx | 17 +- 7 files changed, 239 insertions(+), 75 deletions(-) diff --git a/src/components/modal/project/CreateModalProject.tsx b/src/components/modal/project/CreateModalProject.tsx index 950aa2ac..63407bde 100644 --- a/src/components/modal/project/CreateModalProject.tsx +++ b/src/components/modal/project/CreateModalProject.tsx @@ -4,6 +4,8 @@ import ModalButton from '@components/modal/ModalButton'; import ModalProjectForm from '@components/modal/project/ModalProjectForm'; import type { SubmitHandler } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; +import { useCreateProject } from '@hooks/query/useProjectQuery'; import type { ProjectForm } from '@/types/ProjectType'; type CreateModalProjectProps = { @@ -12,10 +14,13 @@ type CreateModalProjectProps = { export default function CreateModalProject({ onClose: handleClose }: CreateModalProjectProps) { const createProjectFormId = 'createProjectForm'; + const { teamId } = useParams(); + + const numberTeamId = Number(teamId); + const { mutate: createProjectMutate } = useCreateProject(numberTeamId); const handleSubmit: SubmitHandler = async (data) => { - console.log('프로젝트 생성 폼 제출'); - console.log(data); + createProjectMutate(data); handleClose(); }; return ( diff --git a/src/components/modal/project/ModalProjectForm.tsx b/src/components/modal/project/ModalProjectForm.tsx index 1bbea4dd..b3c3526f 100644 --- a/src/components/modal/project/ModalProjectForm.tsx +++ b/src/components/modal/project/ModalProjectForm.tsx @@ -1,6 +1,5 @@ -import { FormProvider, useForm } from 'react-hook-form'; - -import type { SubmitHandler } from 'react-hook-form'; +import { FormProvider, useForm, useWatch } from 'react-hook-form'; +import { DateTime } from 'luxon'; import { useMemo, useState } from 'react'; import RoleTooltip from '@components/common/RoleTooltip'; import { PROJECT_ROLE_INFO, PROJECT_ROLES } from '@constants/role'; @@ -11,28 +10,36 @@ import PeriodDateInput from '@components/common/PeriodDateInput'; import SearchUserInput from '@components/common/SearchUserInput'; import UserRoleSelectBox from '@components/common/UserRoleSelectBox'; import useAxios from '@hooks/useAxios'; -import { findUser } from '@services/userService'; -import { AllSearchCallback } from '@/types/SearchCallbackType'; +import useToast from '@hooks/useToast'; +import { findUserByTeam } from '@services/teamService'; +import { useParams } from 'react-router-dom'; +import type { SubmitHandler } from 'react-hook-form'; +import type { TeamSearchCallback } from '@/types/SearchCallbackType'; import type { ProjectRoleName } from '@/types/RoleType'; -import type { Project, ProjectCoworkerInfo, ProjectForm } from '@/types/ProjectType'; -import type { SearchUser } from '@/types/UserType'; +import type { Project, ProjectCoworker, ProjectForm } from '@/types/ProjectType'; +import type { SearchUser, User } from '@/types/UserType'; type ModalProjectFormProps = { formId: string; - projectId?: Project['projectId']; onSubmit: SubmitHandler; }; -export default function ModalProjectForm({ formId, projectId, onSubmit }: ModalProjectFormProps) { +export default function ModalProjectForm({ formId, onSubmit }: ModalProjectFormProps) { const [showTooltip, setShowTooltip] = useState(false); const [keyword, setKeyword] = useState(''); - const [coworkerInfos, setCoworkerInfos] = useState([]); - // TODO: 프로젝트 생성 팀 사용자 찾기로 바꾸기 - const { loading, data: userList = [], fetchData } = useAxios(findUser); + const [coworkerInfos, setCoworkerInfos] = useState([]); + const { toastInfo } = useToast(); + const { teamId: teamIdString } = useParams(); + const teamId = Number(teamIdString); + const { loading, data: userList = [], clearData, fetchData } = useAxios(findUserByTeam); - // TODO: 프로젝트 생성 팀 사용자 찾기로 바꾸기 - const searchCallbackInfo: AllSearchCallback = useMemo( - () => ({ type: 'ALL', searchCallback: fetchData }), + const searchCallbackInfo: TeamSearchCallback = useMemo( + () => ({ + type: 'TEAM', + searchCallback: (teamId: number, nickname: User['nickname']) => { + return fetchData(teamId, nickname); + }, + }), [fetchData], ); @@ -41,8 +48,8 @@ export default function ModalProjectForm({ formId, projectId, onSubmit }: ModalP defaultValues: { projectName: '', content: '', - startDate: new Date(), - endDate: null, + startDate: DateTime.fromJSDate(new Date()).toFormat('yyyy-LL-dd'), + endDate: DateTime.fromJSDate(new Date()).toFormat('yyyy-LL-dd'), coworkers: [], }, }); @@ -50,38 +57,73 @@ export default function ModalProjectForm({ formId, projectId, onSubmit }: ModalP const { watch, handleSubmit, + setValue, formState: { errors }, register, } = methods; - const startDate = watch('startDate') || new Date(); - const endDate = watch('endDate') || null; + const handleRoleChange = (userId: User['userId'], roleName: ProjectRoleName) => { + const updatedCoworkerInfos = coworkerInfos.map((coworkerInfo) => + coworkerInfo.userId === userId ? { ...coworkerInfo, roleName } : coworkerInfo, + ); - const handleCoworkersClick = (user: SearchUser) => { - console.log(user); + const updatedCoworkers = updatedCoworkerInfos.map(({ userId, roleName, nickname }) => ({ + userId, + roleName, + nickname, + })); + + setValue('coworkers', updatedCoworkers); + setCoworkerInfos(updatedCoworkerInfos); + }; + + const handleRemoveUser = (userId: User['userId']) => { + const filteredCoworkerInfos = coworkerInfos.filter((coworkerInfo) => coworkerInfo.userId !== userId); + const filteredCoworkers = filteredCoworkerInfos.map(({ userId, roleName, nickname }) => ({ + userId, + roleName, + nickname, + })); + + setValue('coworkers', filteredCoworkers); + setCoworkerInfos(filteredCoworkerInfos); }; const handleKeywordChange = (e: React.ChangeEvent) => { setKeyword(e.target.value.trim()); }; - const handleRoleChange = (userId: number, roleName: ProjectRoleName) => { - console.log(userId, roleName); - }; + const handleCoworkersClick = (user: SearchUser) => { + const isIncludedUser = coworkerInfos.find((coworkerInfo) => coworkerInfo.userId === user.userId); + if (isIncludedUser) return toastInfo('이미 포함된 팀원입니다'); - const handleRemoveUser = (userId: number) => { - console.log(userId); + const updatedCoworkerInfos: ProjectCoworker[] = [ + ...coworkerInfos, + { userId: user.userId, nickname: user.nickname, roleName: 'ASSIGNEE' }, + ]; + const updatedCoworkers = updatedCoworkerInfos.map(({ userId, roleName, nickname }) => ({ + userId, + roleName, + nickname, + })); + setCoworkerInfos(updatedCoworkerInfos); + setValue('coworkers', updatedCoworkers); + setKeyword(''); + clearData(); }; + const handleSubmitForm: SubmitHandler = (formData: ProjectForm) => onSubmit(formData); + return ( -
+
setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)}>

프로젝트 권한 정보

+ -
- -
+ + + -
- -
- {coworkerInfos.map(({ userId, nickname }) => ( - - ))} -
+ +
+ {coworkerInfos.map(({ userId, nickname }) => ( + + ))}
diff --git a/src/hooks/query/useProjectQuery.ts b/src/hooks/query/useProjectQuery.ts index 0251f196..d55a6ba3 100644 --- a/src/hooks/query/useProjectQuery.ts +++ b/src/hooks/query/useProjectQuery.ts @@ -1,10 +1,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { generateProjectsQueryKey, generateProjectUsersQueryKey } from '@utils/queryKeyGenerator'; -import { deleteProject, getProjectList, getProjectUserRoleList } from '@services/projectService'; +import { createProject, deleteProject, getProjectList, getProjectUserRoleList } from '@services/projectService'; import useToast from '@hooks/useToast'; import type { Team } from '@/types/TeamType'; -import type { Project } from '@/types/ProjectType'; +import type { Project, ProjectForm } from '@/types/ProjectType'; // Todo: Project Query CUD로직 작성하기 // 팀에 속한 프로젝트 목록 조회 @@ -63,3 +63,24 @@ export function useDeleteProject(teamId: Team['teamId']) { return mutation; } + +// 프로젝트 생성 +export function useCreateProject(teamId: Team['teamId']) { + const queryClient = useQueryClient(); + const { toastSuccess, toastError } = useToast(); + const projectsQueryKey = generateProjectsQueryKey(teamId); + + const mutation = useMutation({ + mutationFn: (projectData: ProjectForm) => createProject(teamId, projectData), + onError: (error) => { + console.log(error); + toastError('프로젝트 생성을 실패했습니다. 다시 시도해 주세요.'); + }, + onSuccess: () => { + toastSuccess('프로젝트를 생성하였습니다.'); + queryClient.invalidateQueries({ queryKey: projectsQueryKey }); + }, + }); + + return mutation; +} diff --git a/src/mocks/mockAPI.ts b/src/mocks/mockAPI.ts index efceff53..fc67a1d9 100644 --- a/src/mocks/mockAPI.ts +++ b/src/mocks/mockAPI.ts @@ -17,7 +17,7 @@ import type { Team } from '@/types/TeamType'; import type { Project } from '@/types/ProjectType'; import type { ProjectStatus, ProjectStatusForm } from '@/types/ProjectStatusType'; import type { Task, TaskUpdateForm } from '@/types/TaskType'; -import type { TaskFileForMemory, TaskUser, UploadTaskFile } from '@/types/MockType'; +import type { ProjectUser, TaskFileForMemory, TaskUser, UploadTaskFile } from '@/types/MockType'; /* ===================== 역할(Role) 관련 처리 ===================== */ @@ -26,6 +26,11 @@ export function findRole(roleId: Role['roleId']) { return ROLE_DUMMY.find((role) => role.roleId === roleId); } +// ToDo: 유저 ID로 조회해야하나, 현재 이름을 넘겨주고 있어서 임시로 만든 조회 방법 수정 필요 +export function findRoleByRoleName(roleName: Role['roleName']) { + return ROLE_DUMMY.find((role) => role.roleName === roleName); +} + /* ===================== 유저(User) 관련 처리 ===================== */ // 유저 조회 @@ -40,8 +45,20 @@ export function findTeamUser(teamId: Team['teamId'], userId: User['userId']) { return TEAM_USER_DUMMY.find((teamUser) => teamUser.teamId === teamId && teamUser.userId === userId); } +// 팀에 속한 모든 유저 조회 +export function findAllTeamUsers(teamId: Team['teamId']) { + return TEAM_USER_DUMMY.filter((teamUser) => teamUser.teamId === teamId); +} + +/* ====================== 팀(Team) 관련 처리 ====================== */ + /* ========= 프로젝트에 연결된 유저(Project User) 관련 처리 ========= */ +// 프로젝트와 유저 연결 생성 +export function createProjectUser(newProjectUser: ProjectUser) { + PROJECT_USER_DUMMY.push(newProjectUser); +} + // 프로젝트와 연결된 유저 조회 export function findProjectUser(projectId: Project['projectId'], userId: User['userId']) { return PROJECT_USER_DUMMY.find((projectUser) => projectUser.projectId === projectId && projectUser.userId === userId); @@ -63,6 +80,11 @@ export function deleteAllProjectUser(projectId: Project['projectId']) { /* ================= 프로젝트(Project) 관련 처리 ================= */ +// 프로젝트 생성 +export function createProject(newProject: Project) { + PROJECT_DUMMY.push(newProject); +} + // 프로젝트 조회 export function findProject(projectId: Project['projectId']) { return PROJECT_DUMMY.find((project) => project.projectId === projectId); diff --git a/src/mocks/services/projectServiceHandler.ts b/src/mocks/services/projectServiceHandler.ts index c3630dd7..b03e9a3e 100644 --- a/src/mocks/services/projectServiceHandler.ts +++ b/src/mocks/services/projectServiceHandler.ts @@ -1,5 +1,7 @@ import { http, HttpResponse } from 'msw'; import { + createProject, + createProjectUser, deleteAllProjectStatus, deleteAllProjectUser, deleteAllTask, @@ -14,13 +16,17 @@ import { findProject, findProjectUser, findRole, + findRoleByRoleName, findTeamUser, findUser, } from '@mocks/mockAPI'; +import { PROJECT_DUMMY } from '@mocks/mockData'; import { convertTokenToUserId } from '@utils/converter'; import type { SearchUser, UserWithRole } from '@/types/UserType'; +import { Project, ProjectForm } from '@/types/ProjectType'; const BASE_URL = import.meta.env.VITE_BASE_URL; +let autoIncrementIdForProject = PROJECT_DUMMY.length + 1; const projectServiceHandler = [ // 프로젝트 소속 유저 검색 API @@ -58,6 +64,54 @@ const projectServiceHandler = [ return HttpResponse.json(matchedSearchUsers); }), + // 프로젝트 생성 API + http.post(`${BASE_URL}/team/:teamId/project`, async ({ request, params }) => { + const accessToken = request.headers.get('Authorization'); + const teamId = Number(params.teamId); + const { coworkers, ...projectInfo } = (await request.json()) as ProjectForm; + + if (!accessToken) return new HttpResponse(null, { status: 401 }); + + const userId = convertTokenToUserId(accessToken); + if (!userId) return new HttpResponse(null, { status: 401 }); + + // 팀 접근 권한 확인 + const teamUser = findTeamUser(teamId, userId); + if (!teamUser) return new HttpResponse('매칭되는 팀 역할이 없습니다.', { status: 403 }); + + // 팀 역할별 권한 확인 + const userRole = findRole(teamUser.roleId); + if (!userRole) return new HttpResponse(null, { status: 500 }); + if (!(userRole.roleName === 'HEAD' || userRole.roleName === 'LEADER')) { + return new HttpResponse('팀 생성 권한이 없습니다.', { status: 403 }); + } + + // 프로젝트 생성 + const projectId = autoIncrementIdForProject++; + const newProject: Project = { + projectId, + teamId, + ...projectInfo, + startDate: new Date(projectInfo.startDate), + endDate: projectInfo.endDate ? new Date(projectInfo.endDate) : null, + }; + createProject(newProject); + + // 프로젝트 유저 연결 생성 + // ToDo: 중간에 잘못되면 일관성, 정합성을 유지할 수 없음 수정 필요. + coworkers.push({ userId, roleName: 'ADMIN' }); + for (let i = 0; i < coworkers.length; i++) { + const coworker = coworkers[i]; + const role = findRoleByRoleName(coworker.roleName); + if (!role) return new HttpResponse(null, { status: 500 }); + + const { roleId } = role; + createProjectUser({ userId, projectId, roleId }); + } + + return new HttpResponse(null, { status: 200 }); + }), + // 프로젝트 목록 조회 API http.get(`${BASE_URL}/team/:teamId/project`, ({ request, params }) => { const accessToken = request.headers.get('Authorization'); diff --git a/src/services/projectService.ts b/src/services/projectService.ts index 5d3df710..4dd4ec67 100644 --- a/src/services/projectService.ts +++ b/src/services/projectService.ts @@ -2,7 +2,7 @@ import { authAxios } from '@services/axiosProvider'; import type { AxiosRequestConfig, AxiosResponse } from 'axios'; import type { Team } from '@/types/TeamType'; -import type { Project } from '@/types/ProjectType'; +import type { Project, ProjectForm } from '@/types/ProjectType'; import type { User, SearchUser, UserWithRole } from '@/types/UserType'; /** @@ -59,6 +59,24 @@ export async function getProjectUserRoleList(projectId: Project['projectId'], ax return authAxios.get(`/project/${projectId}/user`, axiosConfig); } +/** + * 프로젝트 생성 API + * + * @export + * @async + * @param {Team['teamId']} teamId - 팀 ID + * @param {ProjectForm} projectData - 프로젝트 생성 정보 객체 + * @param {AxiosRequestConfig} [axiosConfig={}] - axios 요청 옵션 설정 객체 + * @returns {Promise>} + */ +export async function createProject( + teamId: Team['teamId'], + projectData: ProjectForm, + axiosConfig: AxiosRequestConfig = {}, +): Promise> { + return authAxios.post(`/team/${teamId}/project`, projectData, axiosConfig); +} + /** * 프로젝트 삭제 API * diff --git a/src/types/ProjectType.tsx b/src/types/ProjectType.tsx index d23ae0ce..052e68a3 100644 --- a/src/types/ProjectType.tsx +++ b/src/types/ProjectType.tsx @@ -7,18 +7,23 @@ export type Project = { teamId: number; projectName: string; content: string; - startDate: Date | null; + startDate: Date; endDate: Date | null; }; export type ProjectCoworker = { userId: User['userId']; roleName: ProjectRoleName; - nickname?: string; + nickname: string; }; -export type ProjectCoworkerInfo = ProjectCoworker & { nickname: User['nickname'] }; - -export type ProjectForm = Omit & { - coworkers: ProjectCoworker[]; +export type ProjectInfoForm = { + projectName: string; + content: string; + startDate: string; + endDate: string | null; +}; +export type ProjectCoworkerForm = Omit; +export type ProjectForm = ProjectInfoForm & { + coworkers: ProjectCoworkerForm[]; };