React/React_Project

[React] 팀원 구성 박스 구현

cheon seung hyeon 2023. 9. 18. 13:21

기능 요약 

 1. 입력 내용을 State의 배열 형태로 저장

 2. 포지션 추가 시 팀원 구성 박스 생성

 3. 해당 포지션 클릭 시 이름과 역할 추가 생성

 3-1. 이름과 역할이 작성되지 않았다면 생성되지 않음

 4. 생성된 박스과 이름,역할을 삭제

 

1. State 배열 형태로 저장

 

  const [teamInfoBoxes, setTeamInfoBoxes] = useState([
    { id: 1, addInfo: false, infoData: [{ id: 1, name: '', position: '', role: '' }] },
  ]);

팀원 박스와 이름 역활 데이터를 각각 저장하는 2차원 배열 형태로 저장

id : 팀원 박스

addInfo : 팀원 박스를 추가하는 트리거

infoData : 팀원 박스 내 입력한 정보

 

2. 추가 팀원 박스 생성 

  const handleInfoChange = (newData: TeamMember[], boxId: number) => {
    setTeamInfoBoxes((prevBoxes) => {
      const updatedBoxes = prevBoxes.map((box) => {
        if (box.id === boxId) {
          return { ...box, infoData: newData };
        }
        return box;
      });
      return updatedBoxes;
    });
  };

  const handleAddInfoBox = () => {
    const newId = teamInfoBoxes.length + 1;
    const newInfoBox = {
      id: newId,
      addInfo: false,
      infoData: [{ id: 1, name: '', position: '', role: '' }],
    };
    setTeamInfoBoxes([...teamInfoBoxes, newInfoBox]);
  };

현재 팀원 박스의 길이 + 1 의 id를 가진 빈 팀원 박스를 생성

생성된 팀원 박스를 현 State에 업데이트 시킴으로 새로운 팀원 박스를 생성

 

  const handleDeleteInfoBox = (boxId: number) => {
    setTeamInfoBoxes((prevBoxes) => prevBoxes.filter((box) => box.id !== boxId));
  };

filter 함수와 id를 이용해서 삭제 State 삭제 함수 생성

 

 

export const useTeamInfoBoxes = () => {
  const [teamInfoBoxes, setTeamInfoBoxes] = useState([
    { id: 1, addInfo: false, infoData: [{ id: 1, name: '', position: '', role: '' }] },
  ]);

  const handleInfoChange = (newData: TeamMember[], boxId: number) => {
    setTeamInfoBoxes((prevBoxes) => {
      const updatedBoxes = prevBoxes.map((box) => {
        if (box.id === boxId) {
          return { ...box, infoData: newData };
        }
        return box;
      });
      return updatedBoxes;
    });
  };

  const handleAddInfoBox = () => {
    const newId = teamInfoBoxes.length + 1;
    const newInfoBox = {
      id: newId,
      addInfo: false,
      infoData: [{ id: 1, name: '', position: '', role: '' }],
    };
    setTeamInfoBoxes([...teamInfoBoxes, newInfoBox]);
  };

  const handleDeleteInfoBox = (boxId: number) => {
    setTeamInfoBoxes((prevBoxes) => prevBoxes.filter((box) => box.id !== boxId));
  };

  return {
    teamInfoBoxes,
    handleInfoChange,
    handleAddInfoBox,
    handleDeleteInfoBox,
  };
};

해당 함수들을 커스텀 훅으로 만들어 외부 컴포넌트에서 사용하기 쉽게 생성

  const { teamInfoBoxes, handleInfoChange, handleAddInfoBox, handleDeleteInfoBox } =
    useTeamInfoBoxes();

위와 같이 컴포넌트에서 호출해서 사용 

하지만 컴포넌트 1개에 이 함수들을 이용해서 박스 모델을 만들기에는 코드가 너무 길어지기 때문에
TeamInfoInputBox라는 컴포넌트를 만들어서 연결하는 식으로 동작

          <FlexWrapContainer>
            {teamInfoBoxes.map((box) => (
              <TeamInfoInputBox
                key={box.id}
                addInfo={box.addInfo}
                onRemove={() => handleDeleteInfoBox(box.id)}
                onInfoChange={(newData) => handleInfoChange(newData, box.id)}
                infoData={box.infoData}
              />
            ))}
            <TeamInfoInputBox
              onClick={handleAddInfoBox}
              addInfo
              infoData={[{ id: 1, name: '', position: '', role: '' }]}
            />
          </FlexWrapContainer>

 

TeamInfoInputBox의 경우

 

export const TeamInfoInputBox = ({
  addInfo,
  onClick,
  onRemove,
  onInfoChange,
  infoData,
}: {
  addInfo?: boolean;
  onClick?: () => void;
  onRemove?: () => void;
  onInfoChange?: (newData: TeamMember[]) => void;
  infoData: TeamMember[];
}) => {
  const [position, setPosition] = useState('');

  const handleAddInfo = () => {
    // 이름과 역할 모두 작성되지 않았을 때
    if (infoData.some((member) => member.name === '' || member.role === '')) {
      alert('이름과 역할을 모두 작성해주세요.');
    } else {
      const newMember: TeamMember = { id: infoData.length + 1, name: '', position: '', role: '' };
      if (onInfoChange) {
        onInfoChange([...infoData, newMember]);
      }
    }
  };

  const handleDeleteInfo = (index: number) => {
    const updatedData = [...infoData];
    updatedData.splice(index, 1);
    if (onInfoChange) {
      onInfoChange(updatedData);
    }
  };

  return (
    <div
      onClick={onClick}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          onClick?.();
        }
      }}
      role="button"
      tabIndex={0}
    >
      <InfoContainer>
        <InfoUpperContainer>
          <RowContainer gap="3.2rem">
            <InfoInput2
              placeholder="역할을 선택해주세요"
              large
              value={position}
              onChange={(e) => {
                setPosition(e.target.value);
              }}
              color={`${theme.palette.gray.white}`}
            />
            <button type="button" onClick={onRemove}>
              <img src="/img/close.png" alt="Close Icon" />
            </button>
          </RowContainer>
        </InfoUpperContainer>

        <InfoDownContainer>
          <Section gap="0.8">
            {infoData.map((item, index) => (
              <Section key={item.id} gap="0.8">
                <RowContainer gap="2.4rem">
                  <InfoInput2
                    placeholder="이름을 입력해주세요"
                    large
                    value={item.name}
                    onChange={(e) => {
                      const updatedData = [...infoData];
                      updatedData[index].name = e.target.value;
                      updatedData[index].position = position;
                      if (onInfoChange) {
                        onInfoChange(updatedData);
                      }
                    }}
                  />
                  <button type="button" onClick={() => handleDeleteInfo(index)}>
                    <img src="/img/close.png" alt="Close Icon" />
                  </button>
                </RowContainer>

                <InfoInput2
                  placeholder="어떤 역할을 했나요?"
                  value={item.role}
                  onChange={(e) => {
                    const updatedData = [...infoData];
                    updatedData[index].role = e.target.value;
                    if (onInfoChange) {
                      onInfoChange(updatedData);
                    }
                  }}
                />
              </Section>
            ))}
            {!addInfo && (
              <Body5
                onClick={handleAddInfo}
                style={css`
                  ${theme.typography.body2};
                  color: ${theme.palette.primary[500]};
                  text-decoration: ${theme.palette.primary[500]} 0.25rem solid underline;
                  text-underline-offset: 0.5rem;
                  :hover {
                    cursor: pointer;
                  }
                `}
              >
                해당 포지션에 팀원을 더 추가하고 싶어요
              </Body5>
            )}
          </Section>
        </InfoDownContainer>

        <Overlay addInfo={addInfo} />
      </InfoContainer>
    </div>
  );
};

TeamInfoInputBox.defaultProps = { addInfo: false };

해당 컴포넌트에서 이름, 역할을 추가하는 함수들과 삭제하는 함수 정의

  const handleAddInfo = () => {
    // 이름과 역할 모두 작성되지 않았을 때
    if (infoData.some((member) => member.name === '' || member.role === '')) {
      alert('이름과 역할을 모두 작성해주세요.');
    } else {
      const newMember: TeamMember = { id: infoData.length + 1, name: '', position: '', role: '' };
      if (onInfoChange) {
        onInfoChange([...infoData, newMember]);
      }
    }
  };

  const handleDeleteInfo = (index: number) => {
    const updatedData = [...infoData];
    updatedData.splice(index, 1);
    if (onInfoChange) {
      onInfoChange(updatedData);
    }
  };

다른 점은 위의 함수에선 teaminfoboxs 에 팀원 박스 자체를 추가하기 때문에 id가 추가
Teaminfoinputbox에선 팀원 박스 내에 있는 infoData 속 id가 추가