[React] 키워드 추가 삭제 기능

hyejinJo·2025년 1월 9일
0
post-thumbnail

KCMF 프로젝트의 관리자 페이지에서 공통으로 쓰이는 키워드를 관리하는 페이지를 작업했다. 주제, 장르, 오디오로 나뉘고 해당 분류에 따라 쓰이는 키워드를 추가하거나 삭제할 수 있는 기능이며, 해당 키워드들은 전역적으로 다른 페이지에도 적용이 된다.

  • 기존 데이터로 받은 키워드: currentCodeValueList
  • 프론트에서 추가된 키워드: addedCodeValueList
    • 추가할 공통 코드 키워드 목록
    • codeValueList 라는 이름으로 수정 api 요청 body 에 들어감
    • 키워드가 삭제될 때, 기존 데이터의 키워드인지, 프론트에서 생성된 키워드인지 구분하기 뒤해 각자 state 로 생성
  • 기존의 키워드에서 삭제할 키워드가 있는 경우: removedCodeIdList
    • 삭제할 공통 코드 일련번호 목록
    • isRemoveCodeIdList 라는 이름으로 수정 api 요청 body 에 들어감
...

const MediaForm = ({ data }: MediaFormProps) => {
  const { getMediaList } = useMedia();
  const { updateMedia } = useMediaMutation();
  const { mediaId, mediaType, keywordList, updateDt } = data;
  const [currentCodeValueList, setCurrentCodeValueList] = useState<Array<Code>>(
    [...keywordList].reverse(),
  );
  const [addedCodeValueList, setAddedCodeValueList] = useState<Array<string>>([]);
  const [removedCodeIdList, setRemovedCodeIdList] = useState<Array<number>>([]);
...
  const {
    register,
    handleSubmit,
    watch,
    reset,
    formState: { errors },
  } = useForm({
    mode: 'onChange',
    defaultValues: {
      keyword: '',
    },
  });
  const watchedKeyword: string = watch('keyword');

  const onSubmit = () => {
    addKeyword(watchedKeyword);
    reset();
  };

  const handleMediaUpload = async () => {
    try {
      const payload: UpdateMediaRequestType = {
        codeValueList: addedCodeValueList,
        isRemoveCodeIdList: removedCodeIdList,
      };

      await updateMedia(mediaId, payload);
      openAlert('confirmSuccess');
      await getMediaList();
    } catch (error) {
      console.log(error);
    }
  };

  const deleteKeyword = useCallback((id: number, keyword: string) => {
    if (id) {
      setRemovedCodeIdList((prev) => {
        if (prev.includes(id)) return prev;
        return [...prev, id];
      });
      setCurrentCodeValueList((prev) => prev.filter((item) => item.codeId !== id));
    } else {
      // 프론트단에서 추가한 커스텀 키워드일 경우
      setAddedCodeValueList((prev) => prev.filter((item) => item !== keyword));
      setCurrentCodeValueList((prev) => prev.filter((item) => item.codeValue !== keyword));
    }
  }, []);

  const addKeyword = useCallback(
    (keyword: string) => {
      if (currentCodeValueList.some((item) => item.codeValue === keyword)) {
        // 이미 존재하는 코드일 때
        openAlert('duplicateKeyword');
        return;
      } else {
        setAddedCodeValueList((prev) => {
          if (prev.includes(keyword)) return prev;
          return [...prev, keyword];
        });

        const keywordIdNull = {
          codeId: null,
          codeName: mediaType,
          codeValue: keyword,
          description: mediaTypeMap[mediaType],
        };
        setCurrentCodeValueList((prev) => [keywordIdNull, ...prev]);
      }
    },
    [currentCodeValueList, mediaType, mediaTypeMap, openAlert],
  );

  // 커스텀으로 추가된 요소의 경우
  // 추가: setCurrentCodeValueList 에 id 를 null로 가진 요소에 포함시켜 추가
  // 커스텀 요소 제거시 => id 가 null 이며, value 가 '단편영화' 일 경우 제거

  return (
    <>
      <Box className="flex justify-between w-full">
        ...
      </Box>
      <ContentTable mtNone>
        <colgroup>
          <col className="w-2/12" />
          <col className="w-2/12" />
        </colgroup>
        <TableBody>
          <TableRow>
            <ContentTableCell
              header
              required
              className="text-center"
              rowSpan={currentCodeValueList?.length + 1}
            >
              {mediaTypeMap[mediaType]} {mediaType === 'CATEGORY' ? '관리' : '장르'}
            </ContentTableCell>
            <ContentTableCell colSpan={2} className="border-t-1">
              <Box className="flex gap-8">
                <form noValidate onSubmit={handleSubmit(onSubmit)} className="w-[300px]">
                  <TextField
                    {...register('keyword')}
                    error={!!errors.keyword}
                    helperText={errors.keyword?.message as string}
                    fullWidth
                    placeholder="키워드를 입력 후, Enter를 눌러주세요."
                    className="max-w-400"
                  />
                </form>
                <Button
                  onClick={() => {
                    addKeyword(watchedKeyword);
                    reset();
                  }}
                  color="black"
                >
                  추가
                </Button>
              </Box>
            </ContentTableCell>
          </TableRow>
          {currentCodeValueList?.map((keyword, i) => (
            <TableRow key={i}>
              <ContentTableCell className="text-center" header secondary>
                {currentCodeValueList?.length - i}
              </ContentTableCell>
              <ContentTableCell className="flex" colSpan={3}>
                <Typography
                  className="self-center text-center flex-1"
                  variant="body1"
                  color="black"
                >
                  {keyword?.codeValue}
                </Typography>
                <Button
                  onClick={() => deleteKeyword(keyword?.codeId, keyword?.codeValue)}
                  variant="outlined"
                  color="error"
                  size="small"
                >
                  삭제
                </Button>
              </ContentTableCell>
            </TableRow>
          ))}
        </TableBody>
      </ContentTable>
      ...
    </>
  );
};

export default MediaForm;

이미 존재하는 키워드의 경우 현재 입력한 키워드와 codeValue 값을 비교해 경고창을 띄우고 리셋하도록 설정했다.

결과:

주제를 추가 후 로그를 찍었더니 아래와 같이 나타난다.

키워드 수정 후 저장을 눌러 handleMediaUpload 를 실행하면, 최종적으로 제거된 키워드의 id 값과 프론트에서 추가된 키워드 배열값을 백엔드 요청값으로 보내게 된다. 현재 추가된 키워드와 기존 데이터로 내려오는 키워드를 분간하여 state 로 분리하는게 조금 복잡했지만 꽤나 재미있는 작업이었다.

profile
Frontend Developer 💡

0개의 댓글