
지라 이슈 가 데이터를 만들고, Calend API에 매핑하는 작업을 완료하였다. 날짜를 렌더링하는 컴포넌트와, 이슈 목록을 렌더링하는 컴포넌트를 분할해서 만들었는데 잘 작동하는 듯 하다. 이제 서버와 통신하며, 실제 이슈 목록을 가져오고, 매핑이 성공했을 때 db에 잘 저장만 해주면 된다!
로그인을 한다 → 유저정보를 가지고 있다 → 프로젝트 목록을 가져온다 → 프로젝트 생성페이지로 간다 → 프로젝트를 생성한다 → 생성한 프로젝트 페이지로 이동한다 → 캘린더로 간다 → 지라 이슈 데이터를 가져온다 → 매핑을 시도한다 → db에 저장되는지 확인한다 → 모든 팀원의 이슈가 공유되는지 확인한다
프로젝트를 만드는 생성 페이지를 제작하였다. react-slick 을 통해 총 3가지 단계로 프로젝트에 필요한 정보를 작성하도록 했다.
프로젝트 생성 페이지의 기능은 다음과 같다.
// LIBRARY
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Slider from 'react-slick';
// HOOKS
import { useDeleteProject, useGetProjects } from 'hooks/project';
// STYLE
import {
  StyledContainer,
  StyledSliderContainer,
  StyledPadding,
  StyledFlexColCenter,
  StyledMarginY,
  StyledFlex,
} from './style';
import { theme } from 'styles/theme';
// MOLECULES
import ProjectCreate from 'components/molecules/ProjectCreate';
import JiraLinkageToken from 'components/molecules/JiraLinkageToken';
import GitLabLinkageToken from 'components/molecules/GitLabLinkageToken';
// ATOMS
import FillButton from 'components/atoms/FillButton';
import Sheet from 'components/atoms/Sheet';
/**
 * @description
 * 프로젝트 생성 페이지, 지라와 깃을 연동하고
 * 지라의 프로젝트를 가져와 서비스의 프로젝트와 연결하도록 해주는 페이지
 *
 * @author bell
 */
const index = () => {
  const navigate = useNavigate();
  // project 생성시 받을 프로젝트 id 값
  const [projectId, setProjectId] = useState<number>();
  // 프로젝트가 정상적으로 생성되었는지 체크
  const [isCreated, setIsCreated] = useState<boolean>(false);
  // 지라 프로젝트가 정상적으로 연동되었는지 체크
  const [isLinkedJira, setIsLinkedJira] = useState<boolean>(false);
  // 깃 리포지토리가 정상적으로 연동되었는지 체크
  const [isLinkedGitLab, setIsLinkedGitLab] = useState<boolean>(false);
  const deleteProject = useDeleteProject();
  const getProjects = useGetProjects();
  const settings = {
    dots: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1,
  };
  return (
    <StyledContainer>
      <StyledSliderContainer>
        {/* 슬라이더 기능 */}
        <Slider {...settings}>
          <ProjectCreate setIsCreated={setIsCreated} setProjectId={setProjectId} />
          <JiraLinkageToken
            setIsLinkedJira={setIsLinkedJira}
            projectId={projectId}
          ></JiraLinkageToken>
          <GitLabLinkageToken
            setIsLinkedGitLab={setIsLinkedGitLab}
            projectId={projectId}
          ></GitLabLinkageToken>
          <StyledPadding>
            <Sheet width="100%" height={'50vh'} isShadow={true}>
              <StyledPadding style={{ width: '100%' }}>
                <StyledFlexColCenter>
                  {isCreated && isLinkedJira && isLinkedGitLab ? (
                    <>
                      <h2>
                        프로젝트 생성 및 지라 프로젝트,
                        <br />깃 리포지토리 연동이 모두 성공적으로 완료되었습니다!
                      </h2>
                      <StyledMarginY />
                      <StyledMarginY />
                      <FillButton
                        width="200px"
                        backgroundColor={theme.color.primary}
                        hoverColor={theme.color.secondary}
                        clickHandler={() => {
                          getProjects.refetch();
                          setIsCreated(false);
                          setIsLinkedGitLab(false);
                          setIsLinkedJira(false);
                          navigate('/projects');
                        }}
                      >
                        프로젝트 선택 페이지로 이동
                      </FillButton>
                    </>
                  ) : (
                    <>
                      <StyledFlex>
                        <h2>
                          프로젝트 생성 혹은 지라 프로젝트, 깃 리포지토리 연동 도중 문제가
                          발생하였습니다.
                        </h2>
                      </StyledFlex>
                      <StyledMarginY />
                      <StyledMarginY />
                      <FillButton
                        width="200px"
                        backgroundColor={theme.color.bug}
                        hoverColor={theme.color.primary}
                        clickHandler={() => {
                          deleteProject.mutateAsync({ projectId: projectId as number }).then(() => {
                            getProjects.refetch();
                          });
                          setIsCreated(false);
                          setIsLinkedGitLab(false);
                          setIsLinkedJira(false);
                          navigate('/projects');
                        }}
                      >
                        나가기
                      </FillButton>
                    </>
                  )}
                </StyledFlexColCenter>
              </StyledPadding>
            </Sheet>
          </StyledPadding>
        </Slider>
      </StyledSliderContainer>
    </StyledContainer>
  );
};
export default index;




프로젝트의 환경설정을 담당하는 설정 페이지를 제작하였다. 프로젝트 설정 페이지의 기능은 다음과 같다.
// REACT & REACT-ROUTER
import { useState, ChangeEvent, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
// RECOIL
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { updateProjectState } from 'recoil/atoms/project/updateProject';
// REACT-QUERY
import {
  useDeleteFireTeam,
  useGetProject,
  useGetTeamForProject,
  usePostInviteTeam,
  useUpdateProject,
  useUpdateProjectImage,
  useUpdateTeamColor,
  useUpdateTeamRole,
} from 'hooks/project';
import { useGetUserInfoHandler, useGetUserSearch } from 'hooks/user';
import { Divider } from '@mui/material';
// STYLED-COMPONENT
import {
  StyledPadding,
  StyledMarginY,
  StyledFlex,
  StyledFlexRowEnd,
  StyledFlexCenter,
  StyledInputLogo,
  StyledLabel,
  StyledWrapper,
  StyledPaddingSM,
} from './style';
// COMPONENT - ATOMS
import Sheet from 'components/atoms/Sheet';
import Circle from 'components/atoms/Circle';
import Button from 'components/atoms/Button';
import Notification from 'components/atoms/Notification';
import FillButton from 'components/atoms/FillButton';
// COMPONENT - MOLECULES
import SettingAuth from 'components/molecules/SettingAuth';
import SettingColor from 'components/molecules/SettingColor';
import InviteUser from 'components/molecules/InviteUser';
import InputBox from 'components/molecules/InputBox';
import TextAreaBox from 'components/molecules/TextAreaBox';
// ETC
import { theme } from 'styles/theme';
/**
 * @description
 * 프로젝트 정보를 업데이트 하는 프로젝트 설정 페이지
 *
 * @author bell
 */
const index = () => {
  const location = useLocation();
  // const navigate = useNavigate();
  // 프로젝트 ID
  const projectId = +location.pathname.split('/')[2];
  // update 요청시 필요한 recoil 작업
  const { projectName, projectDescription, projectInviteUser } = useRecoilValue(updateProjectState);
  const projectNameSetRecoilState = useSetRecoilState(updateProjectState);
  const projectDescriptionSetRecoilState = useSetRecoilState(updateProjectState);
  const projectInviteUserSerRecoilState = useSetRecoilState(updateProjectState);
  // 프로젝트 API
  const getUserInfo = useGetUserInfoHandler();
  const getProject = useGetProject(projectId);
  const getUserSearch = useGetUserSearch(projectInviteUser);
  const getTeamForProject = useGetTeamForProject(projectId);
  const postInviteTeam = usePostInviteTeam();
  const updateProject = useUpdateProject();
  const updateProjectImage = useUpdateProjectImage();
  const updateTeamRole = useUpdateTeamRole();
  const updateTeamColor = useUpdateTeamColor();
  const deleteFireTeam = useDeleteFireTeam();
  const myInfo = () => {
    if (getTeamForProject.data && getUserInfo.data) {
      const idx = getTeamForProject.data.findIndex(item => item.userId === getUserInfo.data.id);
      if (idx > -1) {
        return getTeamForProject.data[idx];
      }
    }
  };
  // 현재 로그인한 유저의 프로젝트 등급
  const currentAuth = myInfo()?.role.id;
  // project-logo용 state
  const [image, setImage] = useState();
  useEffect(() => {
    // update 요청을 통해 성공하면 getProject를 다시금 불러온다.
    if (updateProject.isSuccess) {
      getProject.refetch();
    }
    if (updateProjectImage.isSuccess) {
      getProject.refetch();
      setImage(undefined);
    }
    if (updateTeamRole.isSuccess) {
      getTeamForProject.refetch();
    }
    if (updateTeamColor.isSuccess) {
      getTeamForProject.refetch();
    }
    if (deleteFireTeam.isSuccess) {
      getTeamForProject.refetch();
    }
    if (postInviteTeam.isSuccess) {
      getTeamForProject.refetch();
      projectDescriptionSetRecoilState(prevData => {
        return { ...prevData, projectInviteUser: '' };
      });
      getUserSearch.remove();
    }
    // getProject가 refetch를 시도하는 경우
    // localStorage를 업데이트하여 탭의 값도 바꾼다!
    if (updateProject.isSuccess && getProject.isRefetching) {
      const newProjectList = [...JSON.parse(localStorage.getItem('project-tab-list') as string)];
      const idx = newProjectList.findIndex(item => item.id === projectId);
      newProjectList[idx].title = getProject.data?.name;
      localStorage.setItem('project-tab-list', JSON.stringify(newProjectList));
    }
  }, [
    updateProject.isSuccess,
    getProject.isRefetching,
    updateProjectImage.isSuccess,
    updateTeamRole.isSuccess,
    updateTeamColor.isSuccess,
    postInviteTeam.isSuccess,
    deleteFireTeam.isSuccess,
  ]);
  useEffect(() => {
    if (projectInviteUser !== '') getUserSearch.refetch();
  }, [projectInviteUser]);
  return (
    <StyledWrapper>
      {updateProject.isSuccess && (
        <Notification
          check={true}
          message={'프로젝트 명과 상세가 정상적으로 수정되었습니다.'}
          width={'300px'}
        ></Notification>
      )}
      {updateProjectImage.isSuccess && (
        <Notification
          check={true}
          message={'프로젝트 로고가 정상적으로 수정되었습니다.'}
          width={'300px'}
        ></Notification>
      )}
      {updateTeamRole.isSuccess && (
        <Notification
          check={true}
          message={'프로젝트 팀원의 권한이 수정되었습니다.'}
          width={'300px'}
        ></Notification>
      )}
      {updateTeamColor.isSuccess && (
        <Notification
          check={true}
          message={'프로젝트 팀원의 색상이 수정되었습니다.'}
          width={'300px'}
        ></Notification>
      )}
      {postInviteTeam.isSuccess && (
        <Notification
          check={true}
          message={'프로젝트에 해당 팀원을 초대하였습니다.'}
          width={'300px'}
        ></Notification>
      )}
      {deleteFireTeam.isSuccess && (
        <Notification
          check={true}
          message={'프로젝트에서 해당 팀원을 강퇴시켰습니다'}
          width={'300px'}
        ></Notification>
      )}
      {currentAuth !== 'DEVELOPER' && getProject.data && (
        <Sheet
          width={'70vw'}
          maxWidth={'900px'}
          height={'100%'}
          maxHeight={'700px'}
          isShadow={true}
        >
          <StyledFlex>
            <StyledPadding>
              <StyledMarginY>
                <StyledFlexCenter>
                  <Circle height="130px" backgroundColor={theme.color.primary}>
                    <Circle
                      height="120px"
                      isImage={true}
                      url={
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                        // @ts-ignore
                        image ? URL.createObjectURL(image.target.files[0]) : getProject.data.image
                      }
                    ></Circle>
                  </Circle>
                  <StyledMarginY>
                    <StyledInputLogo>
                      <input
                        type="file"
                        id="project_update_logo"
                        onChange={(e: ChangeEvent<HTMLInputElement>) => {
                          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                          // @ts-ignore
                          // 원래는 e.target.files[0] 를 직접 주고 싶었다.
                          // 근데 문제는 e.target.files[0]의 타입을 모른다... (안찾아지더라)
                          // 그래서 그냥 e 다주었다.
                          setImage(e);
                        }}
                      />
                    </StyledInputLogo>
                  </StyledMarginY>
                </StyledFlexCenter>
                <StyledFlexRowEnd>
                  <FillButton
                    width="100px"
                    backgroundColor={theme.button.green}
                    isHover={true}
                    clickHandler={() => {
                      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                      // @ts-ignore
                      updateProjectImage.mutate({ projectId, image });
                    }}
                    hoverColor={theme.button.darkgreen}
                  >
                    이미지 수정
                  </FillButton>
                </StyledFlexRowEnd>
              </StyledMarginY>
              <StyledMarginY>
                <InputBox
                  labelName="프로젝트명"
                  isRow={true}
                  containerWidth={'100%'}
                  inputWidth={'70%'}
                  inputHeight={'40px'}
                  labelSize={'1.3rem'}
                  inputValue={getProject.data.name}
                  useSetRecoilState={projectNameSetRecoilState}
                  recoilParam={'projectName'}
                ></InputBox>
              </StyledMarginY>
              <StyledMarginY>
                <TextAreaBox
                  labelName="프로젝트 상세"
                  isRow={true}
                  containerWidth={'100%'}
                  textAreaWidth={'70%'}
                  textAreaHeight={'100px'}
                  labelSize={'1.3rem'}
                  textAreaValue={getProject.data.description}
                  useSetRecoilState={projectDescriptionSetRecoilState}
                  recoilParam={'projectDescription'}
                  nonResize={true}
                ></TextAreaBox>
              </StyledMarginY>
              <StyledMarginY>
                <StyledFlexRowEnd>
                  <FillButton
                    width="100px"
                    backgroundColor={theme.button.green}
                    isHover={true}
                    clickHandler={() => {
                      updateProject.mutate({
                        projectId,
                        projectName,
                        projectDescription,
                      });
                    }}
                    hoverColor={theme.button.darkgreen}
                  >
                    수정
                  </FillButton>
                </StyledFlexRowEnd>
              </StyledMarginY>
            </StyledPadding>
          </StyledFlex>
        </Sheet>
      )}
      {getTeamForProject.data && (
        <StyledMarginY>
          <Sheet width={'70vw'} maxWidth={'900px'} isShadow={true}>
            <StyledFlex>
              <StyledPadding>
                <StyledMarginY>
                  <InputBox
                    labelName="팀원 초대"
                    isRow={true}
                    containerWidth={'100%'}
                    inputWidth={'70%'}
                    inputHeight={'40px'}
                    labelSize={'1.3rem'}
                    inputPlaceHolder={'초대하고 싶은 팀원의 이메일을 적어주세요!'}
                    useSetRecoilState={projectInviteUserSerRecoilState}
                    recoilParam={'projectInviteUser'}
                  ></InputBox>
                </StyledMarginY>
                {getUserSearch.data &&
                  getUserSearch.data.googleUsers.map(({ id, image, name }) => (
                    <InviteUser
                      userImage={image}
                      userName={name}
                      userId={id}
                      projectId={projectId}
                      postInviteTeam={postInviteTeam.mutate}
                    />
                  ))}
                <StyledPaddingSM />
                <Divider />
                <StyledPaddingSM />
                <StyledMarginY>
                  {currentAuth === 'MASTER' && <StyledLabel>팀원 권한 변경</StyledLabel>}
                  {currentAuth === 'MASTER' &&
                    getTeamForProject.data &&
                    getTeamForProject.data.map(
                      ({ role, userImage, userName, projectId, userId }) => (
                        <SettingAuth
                          roleId={role.id}
                          userImage={userImage}
                          userName={userName}
                          projectId={projectId}
                          userId={userId}
                          updateTeamRole={updateTeamRole.mutate}
                          deleteFireTeam={deleteFireTeam.mutate}
                        ></SettingAuth>
                      ),
                    )}
                </StyledMarginY>
                <StyledPaddingSM />
                <Divider />
                <StyledPaddingSM />
                <StyledMarginY>
                  {currentAuth !== 'DEVELOPER' && <StyledLabel>팀원 색상 변경</StyledLabel>}
                  {currentAuth !== 'DEVELOPER' &&
                    getTeamForProject.data &&
                    getTeamForProject.data.map(
                      ({ userName, userImage, userColor, projectId, userId }) => (
                        <SettingColor
                          userImage={userImage}
                          userName={userName}
                          userColor={userColor}
                          projectId={projectId}
                          userId={userId}
                          updateTeamColor={updateTeamColor.mutate}
                        />
                      ),
                    )}
                </StyledMarginY>
              </StyledPadding>
            </StyledFlex>
          </Sheet>
        </StyledMarginY>
      )}
    </StyledWrapper>
  );
};
export default index;


