Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: #200 프로젝트 생성 #240

Merged
merged 6 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/common/DescriptionTextarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ export default function DescriptionTextarea({
const { register } = useFormContext();

return (
<label htmlFor={id}>
<label htmlFor={id} className="flex flex-col">
<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"
className="w-full rounded-md border border-input p-10 text-regular placeholder:text-xs"
placeholder={placeholder}
rows={5}
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
{...(validationRole ? register(fieldName, validationRole) : register(fieldName))}
/>
<div className={`my-5 h-10 text-xs text-error ${errors ? 'visible' : 'invisible'}`}>{errors}</div>
Expand Down
16 changes: 7 additions & 9 deletions src/components/common/PeriodDateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useEffect, useState } from 'react';
import { DateTime } from 'luxon';
import { useFormContext } from 'react-hook-form';
import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';
import { PERIOD_VALIDATION_RULES } from '@constants/formValidationRules';
import ToggleButton from '@components/common/ToggleButton';
import useToast from '@hooks/useToast';

import type { FieldError } from 'react-hook-form';
import type { Project } from '@/types/ProjectType';

type PeriodDateInputProps = {
startDateId: string;
Expand All @@ -15,8 +14,8 @@ type PeriodDateInputProps = {
endDateLabel: string;
startDateFieldName: string;
endDateFieldName: string;
limitStartDate: Project['startDate'];
limitEndDate: Project['endDate'];
limitStartDate?: string | Date | null;
limitEndDate?: string | Date | null;
};

export default function PeriodDateInput({
Expand All @@ -26,8 +25,8 @@ export default function PeriodDateInput({
endDateLabel,
startDateFieldName,
endDateFieldName,
limitStartDate,
limitEndDate,
limitStartDate = null,
limitEndDate = null,
}: PeriodDateInputProps) {
const [hasDeadline, setHasDeadline] = useState(false);
const { toastWarn } = useToast();
Expand Down Expand Up @@ -62,7 +61,7 @@ export default function PeriodDateInput({
id={startDateId}
type="date"
{...register(startDateFieldName, {
...TASK_VALIDATION_RULES.START_DATE(limitStartDate, limitEndDate),
...PERIOD_VALIDATION_RULES.START_DATE(limitStartDate, limitEndDate),
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
onChange: (e) => {
if (!hasDeadline) setValue(endDateFieldName, e.target.value);
},
Expand All @@ -83,14 +82,13 @@ export default function PeriodDateInput({
className={`${hasDeadline ? '' : '!bg-disable outline-none'}`}
readOnly={!hasDeadline}
{...register(endDateFieldName, {
...TASK_VALIDATION_RULES.END_DATE(hasDeadline, limitStartDate, limitEndDate, watch(startDateFieldName)),
...PERIOD_VALIDATION_RULES.END_DATE(hasDeadline, limitStartDate, limitEndDate, watch(startDateFieldName)),
onChange: (e) => {
const startDate = DateTime.fromISO(startDateStr).startOf('day');
const endDate = DateTime.fromISO(e.target.value).startOf('day');
if (startDate > endDate) {
toastWarn('종료일은 시작일과 같거나 이후로 설정해주세요.');
setValue(endDateFieldName, startDateStr);
setHasDeadline(false);
}
},
})}
Expand Down
9 changes: 7 additions & 2 deletions src/components/modal/project/CreateModalProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<ProjectForm> = async (data) => {
console.log('프로젝트 생성 폼 제출');
console.log(data);
createProjectMutate(data);
handleClose();
};
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
return (
Expand Down
165 changes: 102 additions & 63 deletions src/components/modal/project/ModalProjectForm.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<ProjectForm>;
};

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<ProjectCoworkerInfo[]>([]);
// TODO: 프로젝트 생성 팀 사용자 찾기로 바꾸기
const { loading, data: userList = [], fetchData } = useAxios(findUser);
const [coworkerInfos, setCoworkerInfos] = useState<ProjectCoworker[]>([]);
const { toastInfo } = useToast();
const { teamId: teamIdString } = useParams();
const teamId = Number(teamIdString);
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
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],
);

Expand All @@ -41,47 +48,82 @@ 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'),
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
coworkers: [],
},
});

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<HTMLInputElement>) => {
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' },
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
];
const updatedCoworkers = updatedCoworkerInfos.map(({ userId, roleName, nickname }) => ({
userId,
roleName,
nickname,
}));
setCoworkerInfos(updatedCoworkerInfos);
setValue('coworkers', updatedCoworkers);
setKeyword('');
clearData();
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
};

const handleSubmitForm: SubmitHandler<ProjectForm> = (formData: ProjectForm) => onSubmit(formData);

return (
<FormProvider {...methods}>
<form id={formId} className="mb-10 flex grow flex-col justify-center" onSubmit={handleSubmit(onSubmit)}>
<form id={formId} className="mb-10 flex grow flex-col justify-center" onSubmit={handleSubmit(handleSubmitForm)}>
<div className="relative" onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)}>
<p className="text-sky-700">
<strong>프로젝트 권한 정보</strong>
</p>
<RoleTooltip showTooltip={showTooltip} rolesInfo={PROJECT_ROLE_INFO} />
</div>

<DuplicationCheckInput
id="projectName"
label="프로젝트 명"
Expand All @@ -90,51 +132,48 @@ export default function ModalProjectForm({ formId, projectId, onSubmit }: ModalP
errors={errors.projectName?.message}
register={register('projectName', PROJECT_VALIDATION_RULES.PROJECT_NAME)}
/>
<div className="mb-30">
<DescriptionTextarea
id="content"
label="프로젝트 설명"
fieldName="content"
placeholder="프로젝트 내용을 입력해주세요."
validationRole={PROJECT_VALIDATION_RULES.PROJECT_DESCRIPTION}
errors={errors.content?.message}
/>
</div>

<DescriptionTextarea
id="content"
label="프로젝트 설명"
fieldName="content"
placeholder="프로젝트 내용을 입력해주세요."
validationRole={PROJECT_VALIDATION_RULES.PROJECT_DESCRIPTION}
errors={errors.content?.message}
/>

<PeriodDateInput
startDateLabel="시작일"
endDateLabel="종료일"
startDateId="startDate"
endDateId="endDate"
startDateFieldName="startDate"
endDateFieldName="endDate"
limitStartDate={startDate}
limitEndDate={endDate}
/>

<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">
{coworkerInfos.map(({ userId, nickname }) => (
<UserRoleSelectBox
key={userId}
userId={userId}
nickname={nickname}
roles={PROJECT_ROLES}
defaultValue="MATE"
onRoleChange={handleRoleChange}
onRemoveUser={handleRemoveUser}
/>
))}
</div>
<SearchUserInput
id="search"
label="팀원"
keyword={keyword}
loading={loading}
userList={userList}
searchId={teamId}
searchCallbackInfo={searchCallbackInfo}
onKeywordChange={handleKeywordChange}
onUserClick={handleCoworkersClick}
/>
<div className="flex flex-wrap">
{coworkerInfos.map(({ userId, nickname }) => (
<UserRoleSelectBox
key={userId}
userId={userId}
nickname={nickname}
roles={PROJECT_ROLES}
defaultValue="MATE"
ice-bear98 marked this conversation as resolved.
Show resolved Hide resolved
onRoleChange={handleRoleChange}
onRemoveUser={handleRemoveUser}
/>
))}
</div>
</form>
</FormProvider>
Expand Down
Loading