Skip to content

Commit

Permalink
Feat: #87 팀 생성 UI 및 로직 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-bear98 committed Sep 25, 2024
1 parent 40ccd8e commit af375cb
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 20 deletions.
24 changes: 24 additions & 0 deletions src/components/common/DescriptionITextarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { UseFormRegisterReturn } from 'react-hook-form';

type DescriptionInputProps = {
id: string;
label: string;
placeholder?: string;
errors?: string;
register?: UseFormRegisterReturn;
};

export default function DescriptionInput({ id, label, placeholder, errors, register }: DescriptionInputProps) {
return (
<label htmlFor={id}>
<h3 className="text-large">{label}</h3>
<textarea
id={id}
className="h-100 w-full rounded-md border border-input p-10 text-regular placeholder:text-xs"
placeholder={placeholder}
{...register}
/>
<div className={`my-5 h-10 text-xs text-error ${errors ? 'visible' : 'invisible'}`}>{errors}</div>
</label>
);
}
14 changes: 11 additions & 3 deletions src/components/common/SearchUserInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type SearchInputProps = {
id: string;
label: string;
keyword: string;
searchId: number;
searchId?: number;
loading: boolean;
userList: SearchUser[];
searchCallbackInfo: SearchCallback;
Expand Down Expand Up @@ -44,10 +44,18 @@ export default function SearchUserInput({
searchCallback(keyword, { signal });
break;
case 'TEAM':
searchCallback(searchId, keyword, { signal });
if (searchId !== undefined) {
searchCallback(searchId, keyword, { signal });
} else {
console.error('팀 인원 검색을 위해 searchId(teamId)가 필요합니다.');
}
break;
case 'PROJECT':
searchCallback(searchId, keyword, { signal });
if (searchId !== undefined) {
searchCallback(searchId, keyword, { signal });
} else {
console.error('프로젝트 인원 검색을 위해 searchId(projectId)가 필요합니다.');
}
break;
default:
exhaustiveCheck(type, '사용자 검색 범위가 올바르지 않습니다.');
Expand Down
40 changes: 40 additions & 0 deletions src/components/common/SelectedUserWithRole.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { IoMdCloseCircle } from 'react-icons/io';
import type { SearchUser } from '@/types/UserType';

type SelectedUserWithRoleProps = {
user: SearchUser;
roles: { value: 'HEAD' | 'LEADER' | 'MATE'; label: string }[];
onRoleChange: (userId: number, role: 'HEAD' | 'LEADER' | 'MATE') => void;
onRemoveUser: (userId: number) => void;
};

export default function SelectedUserWithRole({ user, roles, onRoleChange, onRemoveUser }: SelectedUserWithRoleProps) {
const handleRoleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
onRoleChange(user.userId, event.target.value as 'HEAD' | 'LEADER' | 'MATE');
};

return (
<div className="ml-4 mt-4 flex items-center text-sm">
<select
onChange={handleRoleChange}
className="mr-2 rounded-l-lg border-none bg-gray-200 py-2 pl-4 pr-2"
style={{ appearance: 'none' }}
defaultValue="MATE"
>
{roles.map((role) => (
<option key={role.value} value={role.value}>
{role.label}
</option>
))}
</select>

<div className="flex items-center justify-between rounded-r-lg bg-gray-200 p-2">
<span>{user.nickname}</span>
<button type="button" className="ml-2" onClick={() => onRemoveUser(user.userId)} aria-label="유저 제거">
<IoMdCloseCircle className="size-10 cursor-pointer text-close hover:text-[#DF0000]" />
</button>
</div>
</div>
);
}
14 changes: 9 additions & 5 deletions src/components/modal/team/CreateModalTeam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import ModalFormButton from '@components/modal/ModalFormButton';
import ModalTeamForm from '@components/modal/team/ModalTeamForm';

import type { SubmitHandler } from 'react-hook-form';
import type { Team } from '@/types/TeamType';
import type { TeamForm } from '@/types/TeamType';
import { createTeam } from '@/services/teamService';

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

export default function CreateModalTeam({ onClose: handleClose }: CreateModalProjectStatusProps) {
const handleSubmit: SubmitHandler<Team> = async (data) => {
console.log('팀 생성 폼 제출');
console.log(data);
handleClose();
const handleSubmit: SubmitHandler<TeamForm> = async (data) => {
try {
await createTeam(data);
handleClose();
} catch (error) {
console.error('팀 생성 중 오류 발생:', error);
}
};

return (
Expand Down
171 changes: 163 additions & 8 deletions src/components/modal/team/ModalTeamForm.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,175 @@
import { useForm } from 'react-hook-form';

import { FormProvider, useForm } from 'react-hook-form';
import { useMemo, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import type { Team } from '@/types/TeamType';
import DescriptionITextarea from '@components/common/DescriptionITextarea';
import DuplicationCheckInput from '@components/common/DuplicationCheckInput';
import SearchUserInput from '@components/common/SearchUserInput';
import useAxios from '@hooks/useAxios';
import { findUser } from '@services/userService';
import type { SearchUser } from '@/types/UserType';
import type { Team, TeamForm } from '@/types/TeamType';
import { AllSearchCallback } from '@/types/SearchCallbackType';
import useToast from '@/hooks/useToast';
import SelectedUserWithRole from '@/components/common/SelectedUserWithRole';
import RoleIcon from '@/components/common/RoleIcon';
import { TEAM_VALIDATION_RULES } from '@/constants/formValidationRules';

type ModalTeamFormProps = {
formId: string;
teamId?: Team['teamId'];
onSubmit: SubmitHandler<Team>;
onSubmit: SubmitHandler<TeamForm>;
};

export default function ModalTeamForm({ formId, teamId, onSubmit }: ModalTeamFormProps) {
const { handleSubmit } = useForm<Team>();
const [showTooltip, setShowTooltip] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<{ user: SearchUser; role: 'HEAD' | 'LEADER' | 'MATE' }[]>([]);
const [keyword, setKeyword] = useState('');
const { loading, data: userList = [], clearData, fetchData } = useAxios(findUser);
const { toastInfo } = useToast();

const teamRoles = useMemo(
() => [
{ value: 'HEAD' as const, label: 'HEAD' },
{ value: 'LEADER' as const, label: 'Leader' },
{ value: 'MATE' as const, label: 'Mate' },
],
[],
);

const handleRoleChange = (userId: number, role: 'HEAD' | 'LEADER' | 'MATE') => {
setSelectedUsers((prev) => prev.map((item) => (item.user.userId === userId ? { ...item, role } : item)));
};

const handleRemoveUser = (userId: number) => {
setSelectedUsers((prev) => prev.filter((item) => item.user.userId !== userId));
};

const methods = useForm<TeamForm>({
mode: 'onChange',
defaultValues: {
teamName: '',
content: '',
coworkers: [],
},
});

const {
handleSubmit,
formState: { errors },
} = methods;

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

const searchCallbackInfo: AllSearchCallback = useMemo(
() => ({ type: 'ALL', searchCallback: fetchData }),
[fetchData],
);

const handleCoworkersClick = (user: SearchUser) => {
const isIncludedUser = selectedUsers.find((selectedUser) => selectedUser.user.userId === user.userId);
if (isIncludedUser) return toastInfo('이미 포함된 팀원입니다');

const updatedUsers = [...selectedUsers, { user, role: 'MATE' as const }];
setSelectedUsers(updatedUsers);
setKeyword('');
clearData();
};

const handleSubmitForm: SubmitHandler<TeamForm> = (data) => {
const { teamName, content } = data;

// coworkers 배열 생성
const coworkers = selectedUsers.map(({ user, role }) => ({
userId: user.userId,
roleName: role,
}));

// 최종 데이터 구조
const payload = {
teamName,
content,
coworkers,
};

onSubmit(payload);
};

return (
<form id={formId} className="mb-10 flex grow flex-col justify-center" onSubmit={handleSubmit(onSubmit)}>
123123123
</form>
<FormProvider {...methods}>
<form id={formId} className="mb-10 flex grow flex-col justify-center" onSubmit={handleSubmit(handleSubmitForm)}>
{/* TODO: component 분리 하기 */}
<div className="relative" onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)}>
<p className="text-sky-700">
<strong>팀 권한 정보</strong>
</p>
{showTooltip && (
<div className="absolute left-0 top-full z-10 mt-2 w-max rounded-lg bg-gray-500 p-10 text-white shadow-lg">
<div className="flex items-center">
<RoleIcon roleName="HEAD" />
<strong>HEAD</strong>{' '}
</div>
<p>모든 권한 가능</p>
<div className="flex items-center">
<RoleIcon roleName="LEADER" />
<strong>Leader</strong> <br />
</div>
<p>
팀원 탈퇴(Mate만)
<br /> 프로젝트 생성 권한
<br /> 프로젝트 삭제(본인이 생성한 것만)
</p>
<div className="flex items-center">
<RoleIcon roleName="MATE" />
<strong>Mate</strong>
</div>
<p>프로젝트 읽기만 가능, 수정 및 생성 불가</p>
</div>
)}
</div>

<DuplicationCheckInput
id="teamName"
label="팀명"
value={methods.watch('teamName') || ''}
placeholder="팀명을 입력해주세요."
errors={errors.teamName?.message}
register={methods.register('teamName', TEAM_VALIDATION_RULES.TEAM_NAME)}
/>

<DescriptionITextarea
id="teamDescription"
label="팀 설명"
placeholder="팀에 대한 설명을 입력해주세요."
errors={errors.content?.message}
register={methods.register('content', TEAM_VALIDATION_RULES.TEAM_DESCRIPTION)}
/>

<div className="mb-16">
<SearchUserInput
id="search"
label="팀원"
keyword={keyword}
loading={loading}
userList={userList}
searchCallbackInfo={searchCallbackInfo}
onKeywordChange={handleKeywordChange}
onUserClick={handleCoworkersClick}
/>
<div className="flex flex-wrap">
{selectedUsers.map(({ user, role }) => (
<SelectedUserWithRole
key={user.userId}
user={user}
roles={teamRoles}
onRoleChange={handleRoleChange}
onRemoveUser={handleRemoveUser}
/>
))}
</div>
</div>
</form>
</FormProvider>
);
}
4 changes: 2 additions & 2 deletions src/components/modal/team/UpdateModalTeam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import ModaFormButton from '@components/modal/ModalFormButton';
import ModalTeamForm from '@components/modal/team/ModalTeamForm';

import { SubmitHandler } from 'react-hook-form';
import { Team } from '@/types/TeamType';
import { Team, TeamForm } from '@/types/TeamType';

type UpdateModalTeamProps = {
teamId: Team['teamId'];
onClose: () => void;
};
export default function UpdateModalTeam({ teamId, onClose: handleClose }: UpdateModalTeamProps) {
const handleSubmit: SubmitHandler<Team> = async (data) => {
const handleSubmit: SubmitHandler<TeamForm> = async (data) => {
console.log(teamId, '수정 폼 제출');
console.log(data);
handleClose();
Expand Down
20 changes: 20 additions & 0 deletions src/constants/formValidationRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,23 @@ export const TASK_VALIDATION_RULES = deepFreeze({
validate: getTaskDateValidation(projectStartDate, projectEndDate, taskStartDate),
}),
});

export const TEAM_VALIDATION_RULES = deepFreeze({
TEAM_NAME: {
required: '팀명을 입력해주세요.',
maxLength: {
value: 10,
message: '팀명은 최대 10자리까지 입력 가능합니다.',
},
pattern: {
value: /^[가-힣a-zA-Z0-9]*$/,
message: '팀명은 한글, 영문, 숫자만 포함 가능합니다.',
},
},
TEAM_DESCRIPTION: {
maxLength: {
value: 200,
message: '팀 설명은 최대 200자까지 입력 가능합니다.',
},
},
});
Loading

0 comments on commit af375cb

Please sign in to comment.