체크박스(전체 선택, 개별 선택)

개발공부·2022년 11월 29일
0

* 문제점

  1. 전체 선택 / 해제 눌렀을 때 개별 항목들이 제대로 동작하지 않음
    ▶ checked 여부 확인 후 개별항목과 전체 항목이 연결되게 하기

  2. 체크박스 클릭 시 status="C", 체크박스 해제 시 status="A"
    ▶ saga로 넘길 때 "C"인지 "A"인지 조건문

  3. 일부를 미리 체크하고 전체 체크 / 해제를 누를 경우 누적되서 적용됨
    배열을 초기화 하는 것이 핵심(12.07 수정)

(3. 무엇이 문제였나?) 문제점 전체 체크 누적되서 적용됨

▶ 길이를 담당하는 배열 : checkedWordList
▶ 개별 항목에서 2개 정도 누르고 전체 체크 눌렀을 때 전체 항목이 20개라면 20개가 다 체크가 됬다는 게 아닌 22개가 체크가 됬다고 나옴
▶ 개별 항목에 있는 값이 누적되어 전체 체크에 영향을 줌
초기화가 필요했음(wordSlice 수정함)

배열 초기화 
//1
const array = [1, 2, 3]
array.length = 0;
console.log(array) // []

//2
let array2 = [1, 2, 3]
array2 = []
console.log(array2) // []

* 참고한 블로그 및 사이트

https://myhappyman.tistory.com/115
https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
https://hianna.tistory.com/453
https://nscworld.net/2021/06/16/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%B0%B0%EC%97%B4-%EC%82%AD%EC%A0%9C%ED%95%98%EB%8A%94-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95/

  • toolkit과 관련한 데이터 수정 및 변경 내용
const obj = [
    {id: 123, name: "A"}, 
    {id: 123, name: "B"},
    {id: 123, name: "C"},
    {id: 123, name: "D"},
]

//개별 항목 변경
console.log(obj[0]) //{id: 123, name: 'A'}
obj[0].id = 1
console.log(obj[0]) //{id: 1, name: 'A'}

const obj2 = [
    {id: 123, name: "A"}, 
    {id: 123, name: "B"},
    {id: 123, name: "C"},
    {id: 123, name: "D"},
]

//전체 항목 변경
obj2.map((o, i) => {
    o.id = 1
})

console.log(obj2)

//{id: 1, name: 'A'}
//{id: 1, name: 'B'}
//{id: 1, name: 'C'}
//{id: 1, name: 'D'}

* 결과

1) 전체 선택

2) 개별 선택 후 해제

3) 개별 선택

* 코드 관련

component(화면단)

(WordList.js)

redux-toolkit

(feature/wordSlice.js)

redux-saga

(sagas/wordSaga.js)

* WordList.js

import React, { useCallback, useState } from "react";

import { useDispatch, useSelector } from "react-redux";
import {
  changeStatusWordRequest,
  changeStatusWordAllRequest,
} from "../../redux/feature/wordSlice";

const WordList = () => {
  const dispatch = useDispatch();
  const checkedWordList = [];

  const { wordLists } = useSelector((state) => state.word);


  wordLists.map((word, i) => {
    if (word.status === "C") {
      checkedWordList.push(word);
    }
  });

//전체 선택
  const onClickAllSelected = useCallback((e) => {
    const checkboxClickedAll = e.target;
    checkboxClickedAll.classList.toggle(status);
    const checkboxes = document.querySelectorAll("input[name=checkItem]");

    if (checkboxClickedAll.checked) {
      dispatch(changeStatusWordAllRequest({ status: status }));
      for (let i = 0; i < checkboxes.length; i++) {
        checkboxes[i].checked = checkboxClickedAll.checked;
      }
    } else {
      dispatch(changeStatusWordAllRequest({ status: "A" }));
      for (let i = 0; i < checkboxes.length; i++) {
        checkboxes[i].checked = checkboxClickedAll.checked;
      }
    }
  }, []);

//개별 선택
  const onClickSelected = useCallback((e) => {
    const checkboxClicked = e.target;
    const wordIndex = e.target.value;

    checkboxClicked.classList.toggle(status);

    if (checkboxClicked.checked) {
      dispatch(changeStatusWordRequest({ id: wordIndex, status: status }));
    } else if (!checkboxClicked.checked) {
      dispatch(changeStatusWordRequest({ id: wordIndex, status: "A" }));
    }
  }, []);

  return (
    <>
      {/* checkbox */}
      <div className="absolute top-64 right-0 md:right-20 lg:right-52">
        <div className="flex items-center mb-2">
          <input
            onChange={onClickAllSelected}
            id="checkboxAll"
            type="checkbox"
            className="w-4 h-4 text-light-green bg-gray-900 rounded border-light-green focus:ring-light-green dark:focus:ring-light-green dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
          />
          <label
            htmlFor="checkboxAll"
            className="ml-2 text-sm font-bold text-gray-900 border-light-green"
          >
            전체 선택 / 해제
          </label>
        </div>
        <p>
          체크된 단어 개수 : {checkedWordList.length}/{wordLists.length}
        </p>
      </div>
      
      <div className="lg:w-full relative">
        <div className="h-max mx-20 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-1">
          {/* Easy start */}
          <div className="group relative rounded-lg p-3 lg:w-80 lg:ml-10">
            <div className="overflow-y-auto max-h-96 aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-md bg-white border-2 border-light-green lg:aspect-none">
              <div>
                <h1 className="text-slate-900 font-medium px-3 pt-2">
                  🥉 Easy
                </h1>
              </div>
              {/* <div className="flex absolute left-32 top-4">
                <button className="hover:bg-gray-100 w-8 h-8 rounded-lg">
                  <ChevronLeftIcon onClick={onClickLeftEasy} />
                </button>
                <div className="font-medium mt-1">{page}</div>
                <button className="hover:bg-gray-100 w-8 h-8 rounded-lg">
                  <ChevronRightIcon onClick={onClickRightEasy} />
                  {minIndex} {maxIndex}
                </button>
              </div> */}
              {/* item start */}
              {wordLists.map((word, index) => {
                if (
                  word.type === "easy"
                  return (
                    <>
                      <div
                        key={word}
                        className="flex items-start bg-gray-400 group-hover:opacity-80 rounded-lg m-2"
                      >
                        <div className="h-24 w-90 sm:600 w-96 lg:w-48">
                          <div className="flex py-5 pl-1">
                            <input
                              onClick={onClickSelected}
                              value={index}
                              id="checkItem"
                              name="checkItem"
                              type="checkbox"
                              className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
                            />
                          </div>
                          <li className="flex first:pt-0 last:pb-0">
                            <div className="relative bottom-10 ml-9 overflow-hidden">
                              <p className="text-sm font-medium text-slate-900">
                                {word.english} ({index})
                              </p>
                              <p className="text-sm text-slate-900 truncate">
                                {word.korean} {word.id} {word.status}
                              </p>
                            </div>
                          </li>
                        </div>
                        <div className="relative h-24 w-24 py-2">
                          <Menu
                            as="div"
                            className="relative inline-block text-left"
                          >
                            <div>
                              <Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-gray-100">
                                Options
                                <ChevronDownIcon
                                  className="ml-1 h-5 w-5"
                                  aria-hidden="true"
                                />
                              </Menu.Button>
                            </div>

                            <Transition
                              as={Fragment}
                              enter="transition ease-out duration-90"
                              enterFrom="transform opacity-0 scale-95"
                              enterTo="transform opacity-100 scale-100"
                              leave="transition ease-in duration-75"
                              leaveFrom="transform opacity-100 scale-100"
                              leaveTo="transform opacity-0 scale-95"
                            >
                              <Menu.Items className="absolute right-0 z-10 mt-2 w-24 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
                                <div className="py-0">
                                  <Menu.Item>
                                    {({ active }) => (
                                      <button
                                        value={index}
                                        onClick={onReviseWord}
                                        className={
                                          (active
                                            ? "rounded-md bg-gray-100 text-gray-900"
                                            : "rounded-md text-gray-700 ",
                                          "block px-8 py-1 text-sm hover:bg-light-green hover:w-24 hover:text-white")
                                        }
                                      >
                                        수정
                                      </button>
                                    )}
                                  </Menu.Item>
                                  <Menu.Item>
                                    {({ active }) => (
                                      <button
                                        value={index}
                                        onClick={onRemoveWord}
                                        className={
                                          (active
                                            ? "bg-gray-100 text-gray-900"
                                            : "text-gray-700",
                                          "block px-8 py-1 text-sm hover:bg-light-green hover:w-24 hover:text-white")
                                        }
                                      >
                                        삭제
                                      </button>
                                    )}
                                  </Menu.Item>
                                </div>
                              </Menu.Items>
                            </Transition>
                          </Menu>
                        </div>
                      </div>
                    </>
                  );
                }
              })}
              {/* item end */}
            </div>
          </div>
          {/* Easy end */}
          {/* Middle start */}
          <div className="group relative rounded-lg p-3 lg:w-80 lg:ml-10">
            <div className="overflow-y-auto max-h-96 aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-md bg-white border-2 border-light-green lg:aspect-none">
              <div>
                <h1 className="text-slate-900 font-medium px-3 pt-2">
                  🥈 Middle
                </h1>
              </div>
              {/* <div className="flex absolute left-32 top-4">
                <button className="hover:bg-gray-100 w-8 h-8 rounded-lg">
                  <ChevronLeftIcon onClick={onClickLeftMiddle} />
                </button>
                <div className="font-medium mt-1">{pageMiddle}</div>
                <button className="hover:bg-gray-100 w-8 h-8 rounded-lg">
                  <ChevronRightIcon onClick={onClickRightMiddle} />
                  {minIndexMiddle} {maxIndexMiddle}
                </button>
              </div> */}
              {/* item start */}
              {wordLists.map((word, index) => {
                if (
                  word.type === "middle"
                  {
                    return (
                      <>
                        <div
                          key={word}
                          className="flex items-start bg-gray-400 group-hover:opacity-80 rounded-lg m-2"
                        >
                          <div className="h-24 w-90 sm:600 w-96 lg:w-48">
                            <div className="flex py-5 pl-1">
                              <input
                                onClick={onClickSelected}
                                value={index}
                                id="checkItem"
                                name="checkItem"
                                type="checkbox"
                                className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
                              />
                            </div>
                            <li className="flex first:pt-0 last:pb-0">
                              <div className="relative bottom-10 ml-9 overflow-hidden">
                                <p className="text-sm font-medium text-slate-900">
                                  {word.english} ({index})
                                </p>
                                <p className="text-sm text-slate-900 truncate">
                                  {word.korean} {word.id} {word.status}
                                </p>
                              </div>
                            </li>
                          </div>
                          <div className="relative h-24 w-24 py-2">
                            <Menu
                              as="div"
                              className="relative inline-block text-left"
                            >
                              <div>
                                <Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-gray-100">
                                  Options
                                  <ChevronDownIcon
                                    className="ml-1 h-5 w-5"
                                    aria-hidden="true"
                                  />
                                </Menu.Button>
                              </div>

                              <Transition
                                as={Fragment}
                                enter="transition ease-out duration-90"
                                enterFrom="transform opacity-0 scale-95"
                                enterTo="transform opacity-100 scale-100"
                                leave="transition ease-in duration-75"
                                leaveFrom="transform opacity-100 scale-100"
                                leaveTo="transform opacity-0 scale-95"
                              >
                                <Menu.Items className="absolute right-0 z-10 mt-2 w-24 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
                                  <div className="py-0">
                                    <Menu.Item>
                                      {({ active }) => (
                                        <button
                                          value={index}
                                          onClick={onReviseWord}
                                          className={
                                            (active
                                              ? "rounded-md bg-gray-100 text-gray-900"
                                              : "rounded-md text-gray-700 ",
                                            "block px-8 py-1 text-sm hover:bg-light-green hover:w-24 hover:text-white")
                                          }
                                        >
                                          수정
                                        </button>
                                      )}
                                    </Menu.Item>
                                    <Menu.Item>
                                      {({ active }) => (
                                        <button
                                          value={index}
                                          onClick={onRemoveWord}
                                          className={
                                            (active
                                              ? "bg-gray-100 text-gray-900"
                                              : "text-gray-700",
                                            "block px-8 py-1 text-sm hover:bg-light-green hover:w-24 hover:text-white")
                                          }
                                        >
                                          삭제
                                        </button>
                                      )}
                                    </Menu.Item>
                                  </div>
                                </Menu.Items>
                              </Transition>
                            </Menu>
                          </div>
                        </div>
                      </>
                    );
                  }
                }
              })}
              {/* item end */}
            </div>
          </div>
          {/* Middle end */}
          {/* Advance start */}
          <div className="group relative rounded-lg p-3 lg:w-80 lg:ml-10">
            <div className=" overflow-y-auto max-h-80 aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-md bg-white border-2 border-light-green lg:aspect-none">
              <div>
                <h1 className="text-slate-900 font-medium px-3 pt-2">
                  🥇 Advance
                </h1>
              </div>
              {/* item start */}
              {wordLists.map((word, index) => {
                if (word.type === "advance") {
                  return (
                    <>
                      <div className="flex items-start bg-gray-400 group-hover:opacity-80 rounded-lg m-2">
                        <div className="h-24 w-90 sm:600 w-96 lg:w-48">
                          <div className="flex py-5 pl-1">
                            <input
                              onClick={onClickSelected}
                              value={index}
                              id="checkItem"
                              name="checkItem"
                              type="checkbox"
                              className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
                            />
                          </div>
                          <li className="flex first:pt-0 last:pb-0">
                            <div className="relative bottom-10 ml-9 overflow-hidden">
                              <p className="text-sm font-medium text-slate-900">
                                {word.english} ({index})
                              </p>
                              <p className="text-sm text-slate-900 truncate">
                                {word.korean} {word.id} {word.status}
                              </p>
                            </div>
                          </li>
                        </div>
                        <div className="relative h-24 w-24 py-2">
                          <Menu
                            as="div"
                            className="relative inline-block text-left"
                          >
                            <div>
                              <Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-gray-100">
                                Options
                                <ChevronDownIcon
                                  className="ml-1 h-5 w-5"
                                  aria-hidden="true"
                                />
                              </Menu.Button>
                            </div>

                            <Transition
                              as={Fragment}
                              enter="transition ease-out duration-90"
                              enterFrom="transform opacity-0 scale-95"
                              enterTo="transform opacity-100 scale-100"
                              leave="transition ease-in duration-75"
                              leaveFrom="transform opacity-100 scale-100"
                              leaveTo="transform opacity-0 scale-95"
                            >
                              <Menu.Items className="absolute right-0 z-10 mt-2 w-24 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
                                <div className="py-0">
                                  <Menu.Item>
                                    {({ active }) => (
                                      <button
                                        value={index}
                                        onClick={onReviseWord}
                                        className={
                                          (active
                                            ? "rounded-md bg-gray-100 text-gray-900"
                                            : "rounded-md text-gray-700 ",
                                          "block px-8 py-1 text-sm hover:bg-light-green hover:w-24 hover:text-white")
                                        }
                                      >
                                        수정
                                      </button>
                                    )}
                                  </Menu.Item>
                                  <Menu.Item>
                                    {({ active }) => (
                                      <button
                                        value={index}
                                        onClick={onRemoveWord}
                                        className={
                                          (active
                                            ? "bg-gray-100 text-gray-900"
                                            : "text-gray-700",
                                          "block px-8 py-1 text-sm hover:bg-light-green hover:w-24 hover:text-white")
                                        }
                                      >
                                        삭제
                                      </button>
                                    )}
                                  </Menu.Item>
                                </div>
                              </Menu.Items>
                            </Transition>
                          </Menu>
                        </div>
                      </div>
                    </>
                  );
                }
              })}
              {/* item end */}
            </div>
          </div>
          {/* Advance end */}
        </div>
      </div>
    </>
  );
};

export default WordList;

* feature/wordSlice.js

const initialState = {
  wordLists: [
    {
      id: 2,
      english: "red",
      korean: "빨강",
      type: "easy",
      status: "A",
    },
    {
      id: 4,
      english: "blue",
      korean: "파랑",
      type: "easy",
      status: "A",
    },
    {
      id: 3,
      english: "blue2",
      korean: "파랑2",
      type: "easy",
      status: "A",
    },
    ...
  ],
  changeStatusWordLoading: false, //단어 상태 수정
  changeStatusWordComplete: false,
  changeStatusWordError: null,
};

   //개별 status 바꾸기
    changeStatusWordRequest: (state) => {
      state.changeStatusWordLoading = true;
      state.changeStatusWordError = null;
      state.changeStatusWordComplete = false;
    },
    changeStatusWordSuccess: (state, action) => {
      const wordInfo = action.payload;
      state.changeStatusWordLoading = false;
      state.changeStatusWordComplete = true;

      state.wordLists[wordInfo.id].status = wordInfo.status;
    },
    changeStatusWordError: (state, action) => {
      state.changeStatusWordLoading = true;
      state.changeStatusWordError = action.error;
    },
    //전체 status 바꾸기
    changeStatusWordAllRequest: (state) => {
      state.changeStatusWordLoading = true;
      state.changeStatusWordError = null;
      state.changeStatusWordComplete = false;
    },
    changeStatusWordAllSuccess: (state, action) => {
      **const wordInfo = action.payload;
      state.changeStatusWordLoading = false;
      state.changeStatusWordComplete = true;

	  //이 부분 적용
      state.checkedWordList.length = 0;

      state.wordLists.map((word, i) => {
        word.status = wordInfo.status;
        if (word.status === "C") {
          state.checkedWordList.push(state.wordLists[i]);
        } else if (word.status === "A") {
          state.checkedWordList.pop();
        }
      });**
    },
    changeStatusWordAllError: (state, action) => {
      state.changeStatusWordLoading = true;
      state.changeStatusWordError = action.error;
    },
    
    export const {
  changeStatusWordRequest,
  changeStatusWordSuccess,
  changeStatusWordError,
  changeStatusWordAllRequest,
  changeStatusWordAllSuccess,
  changeStatusWordAllError,
} = wordSlice.actions;

* sagas/wordSaga.js

function* changeStatus(action) {
  try {
    const data = action.payload;
    yield put(changeStatusWordSuccess(data));
  } catch (error) {
    yield put(changeStatusWordError(error));
    console.log(error);
  }
}

function* changeStatusAll(action) {
  try {
    const data = action.payload;
    yield put(changeStatusWordAllSuccess(data));
  } catch (error) {
    yield put(changeStatusWordAllError(error));
    console.log(error);
  }
}

function* change_Status_Req() {
  yield takeLatest(changeStatusWordRequest.type, changeStatus);
}

function* change_StatusAll_Req() {
  yield takeLatest(changeStatusWordAllRequest.type, changeStatusAll);
}

export const wordSagas = [
  fork(change_Status_Req),
  fork(change_StatusAll_Req),
];
profile
개발 블로그, 티스토리(https://ba-gotocode131.tistory.com/)로 갈아탐

0개의 댓글