221102 자율 프로젝트 개발일지

·2022년 11월 25일
0

개발일지 & 회고

목록 보기
65/72
post-thumbnail

✍ 오늘 한 일

💡 오늘 진행 상황을 간단하게 정리 합니다.

매핑 성공

지라 이슈 가 데이터를 만들고, 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;

프로젝트 설정 페이지

프로젝트의 환경설정을 담당하는 설정 페이지를 제작하였다. 프로젝트 설정 페이지의 기능은 다음과 같다.

  • 프로젝트 이름과 설명 그리고 이미지를 새롭게 수정하는 것이 가능하다.
  • 해당 프로젝트에 팀원을 초대하는 것이 가능하다.
  • 프로젝트 팀원의 권한을 설정하는 것이 가능하다.
  • 만약 마스터 권한을 다른 팀원에게 주는 경우, 자신의 권한이 메인테이너로 바뀌게 된다. (마스터는 오직 프로젝트 한명)
  • 팀원의 고유 색을 설정하는 것이 가능하다.
  • 팀원을 강퇴 할 수 있다.
  • 권한에 따라 프로젝트를 설정할 수 영역이 다르다.
  • 마스터 - 모든 설정 가능
  • 메인테이너 - 프로젝트 정보 수정 및 팀원 초대
  • 디벨로퍼 - 설정x, 설정 페이지에 아예 접근하지 못하도록 설정
// 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;

📢 개선 사항

💡 오늘 하루 개선하면 좋았을 부분을 작성합니다. 없다면 생략하셔도 좋습니다.

📢 내일 진행 예정

💡 내일 할 업무를 팀원들과 함께 공유해봅시다. 글이 자세할수록, 팀원 모두가 업무 흐름을 파악하기 쉬워집니다.
profile
새로운 것에 관심이 많고, 프로젝트 설계 및 최적화를 좋아합니다.

0개의 댓글