[PWA]리뷰관리 admin 페이지 연동(3) - 삭제요청

Kimmy·2025년 4월 9일

PWA_PROJECT

목록 보기
11/47

관리자페이지 리뷰관리에서 삭제권한 없애기

개발관점이 아니라 사업관점에서 보았을때, 매장에서 내가 남긴 리뷰를 마음대로 삭제한다면 그 매장뿐만 아니라 해당 플랫폼의 신뢰도가 낮아질것이다. 그래서 관리자페이지에 있던 삭제기능을 없애고 대신 삭제요청 으로 대체하여, 플랫폼 운영자가 1주일에 한번정도 삭제요청들어온 리뷰를 일괄 삭제처리 하는 방식으로 해야할 것 같다.

먼저 코드에서 기존 버튼을 지워주고 '삭제요청'으로 버튼 이름을 바꿔준다. 바로 삭제요청이 되지 않고, Modal을 띄워서 삭제요청에 대한 적절한 사유를 선택하고 제출하도록 해보려고한다.

common components에 Modal 컴포넌트가 있어서, 새로 만들지 않고 이것을 가져와서 사용했다.
삭제 요청 버튼을 클릭하면 모달창이 뜨고 거기에 "운영자에게 해당 리뷰를 삭제요청하시겠습니까?" 라는 문구와 함께, 사유를 선택해서 제출 할 수 있도록 하고, 사유는 부적합한 애용, 스팸, 허위정보 3개의 선택지와 기타란도 추가해서 직접 관리자가 작성 할 수 있도록해보겠다.

삭제요청 버튼 무반응- 공통컴포넌트 활용

먼저 각각 모달 상태, 선택된 리뷰ID, 선택된 사유, 기타 사유 입력값 useState를 설정해주었다.

export default function AdminReviewsPage() {
  const [isModalOpen, setIsModalOpen] = useState(false); // 모달 상태
  const [selectedReviewId, setSelectedReviewId] = useState(null); // 선택된 리뷰 ID
  const [reason, setReason] = useState(""); // 선택된 사유
  const [customReason, setCustomReason] = useState(""); // 기타 사유 입력값

삭제 요청 버튼을 누르면 모달창이 뜨도록 코드를 수정했는데 삭제 요청 버튼 눌러도 무반응이었다.

문제는 가져온 공통Modal 컴포넌트가 isOpen prop을 사용하여 모달의 열림/닫힘 상태를 제어하고 있는데, AdminReviewsPage에서 Modal 컴포넌트를 사용할 때 isOpen prop을 전달하지 않고 isModalOpen를 사용하고있었기 때문이다.

변경전

{isModalOpen && (
  <Modal
    title="리뷰 삭제 요청"
    onClose={() => setIsModalOpen(false)}
  >
    {/* 모달 내용 */}
  </Modal>
)}

변경 전 방식은 isOpen prop을 사용하지 않으므로 Modal 컴포넌트 내부의 if (!isOpen) return null; 조건에 의해 모달이 렌더링되지 않았다.

변경후

<Modal
  isOpen={isModalOpen} // isOpen prop 전달
  title="리뷰 삭제 요청"
  onClose={() => setIsModalOpen(false)} // 모달 닫기
>

Modal 컴포넌트에 isOpen prop을 전달하여 모달의 열림/닫힘 상태를 제어하도록 수정하였다.

변경 후 동작

isOpen prop 전달:

isModalOpen 상태가 true일 때 isOpen={true}가 전달되어 모달이 렌더링된다.
isModalOpen 상태가 false일 때 isOpen={false}가 전달되어 모달이 렌더링되지 않는다.

onClose 콜백:
모달의 닫기 버튼이나 배경을 클릭하면 onClose 콜백이 호출되어 setIsModalOpen(false)가 실행된다.

사유선택 안하면 제출버튼 비활성화시키기

동작 방식

reason와 customReason이 비어 있으므로 제출 버튼이 비활성화된다.

사용자가 사유를 선택하면 reason 값이 설정되고 제출 버튼이 활성화된다.

사용자가 "기타"를 선택한 경우, customReason에 값이 입력되어야 제출 버튼이 활성화된다.

제출 버튼을 클릭하면 선택된 사유 또는 입력된 기타 사유가 콘솔에 출력되고 모달이 닫힌다.

문제발생_동작안함

reason과 customReason이 초기화되어 제출 버튼이 비활성화되야하는데 안된다.. 사용자가 사유를 선택하면 reason 값이 설정되고 제출 버튼이 활성화되야하는데, 삭제요청 눌러서 모달창 켜지면 이미 제출 버튼이 활성화되어져 있는 문제가 있었다.

찾아보니,
제출 버튼이 모달이 열리자마자 활성화되어 있는 이유는 React의 상태 업데이트는 비동기적으로 이루어지기 때문에, 초기화가 완료되기 전에 reason 상태가 이전 값으로 간주될 수 있다고했다.

  1. 문제의 원인
    React에서 setState(예: setReason(""))를 호출하면 상태가 즉시 변경되는 것이 아니라, 비동기적으로 업데이트된다. 즉, 상태가 변경되기 전에 컴포넌트가 다시 렌더링될 수 있는것.

문제 상황:
handleDeleteReviewRequest 함수에서 setReason("")와 setCustomReason("")를 호출하여 상태를 초기화했지만, 상태가 즉시 반영되지 않았다.

이로 인해, 모달이 열릴 때 reason 상태가 여전히 이전 값(또는 초기값)으로 간주되어 제출 버튼이 활성화된 상태로 렌더링되었다.

  1. useEffect로 해결하기
    useEffect를 사용하여 isModalOpen 상태가 true로 변경될 때 reason과 customReason 상태를 초기화했다
useEffect(() => {
  if (isModalOpen) {
    setReason(""); // 사유 초기화
    setCustomReason(""); // 기타 사유 초기화
  }
}, [isModalOpen]);

useEffect는 의존성 배열([])에 지정된 상태나 props가 변경될 때 실행됩니다.
isModalOpen이 true로 변경되면 useEffect가 실행되어 reason과 customReason 상태를 초기화한다.

이 초기화는 모달이 열리기 전에 완료되므로, 모달이 렌더링될 때 reason과 customReason 상태가 빈 값으로 설정된다.

결과적으로, 제출 버튼의 disabled 조건이 올바르게 평가되어 버튼이 비활성화된다.

  1. React 상태 업데이트의 비동기적 특성
    React의 상태 업데이트는 비동기적으로 이루어지며, 상태 변경이 즉시 반영되지 않을 수 있다.

예시)

setReason(""); // 상태 업데이트 요청
console.log(reason); // 이전 상태가 출력됨 (업데이트된 상태가 아님)

이유를 찾아보니:
React는 성능 최적화를 위해 여러 상태 업데이트를 배치(batch) 처리한다.
상태가 변경되더라도, React는 다음 렌더링 주기에서 변경된 상태를 반영한다.

  1. useEffect의 역할
    useEffect는 상태 변경이나 특정 이벤트가 발생한 후에 실행되는 사이드 이펙트 처리를 위한 훅이다. 이를 통해 상태 변경이 완료된 후 추가 작업을 수행할 수 있다.

useEffect의 동작:
isModalOpen 상태가 true로 변경된다.
React는 컴포넌트를 다시 렌더링한다.
렌더링이 완료된 후, useEffect가 실행되어 reason과 customReason 상태를 초기화한다.

  1. 왜 useEffect가 적합한 해결책인지?
    useEffect는 상태 변경 후 실행되므로, 상태 초기화가 정확한 시점에 이루어진다.
    isModalOpen 상태를 의존성으로 지정했기 때문에, 모달이 열릴 때마다 상태를 초기화할 수 있다.
    이를 통해 모달이 열릴 때 항상 reason과 customReason 상태가 초기화된 상태로 렌더링된다.

  2. 정리
    React 상태 업데이트의 비동기적 특성을 보완하기 위해, 상태 변경 후 실행되는 useEffect를 사용했다.
    isModalOpen 상태를 의존성으로 지정하여, 모달이 열릴 때마다 상태를 초기화하였다.
    이를 통해 제출 버튼의 disabled 조건이 올바르게 평가되도록 보장함.
    결과적으로, 모달이 열릴 때 제출 버튼이 비활성화된 상태로 렌더링되며, 사용자가 사유를 선택하거나 입력해야만 활성화된다.

비활성화 코드를 다시 삭제

목표: 사유를 선택하지않으면 제출불가 하도록하기
현황:

  • 제출버튼을 누르면 사유를 선택하라는 alert가 뜨지않고
  • 제출버튼은 비활성화되어있음

비활성화는 잘 되어있는데, alert가 안뜨는 문제가 있었다.

찾아보니 사용자가 사유를 선택하지 않고 제출 버튼을 누르려고 할 때 경고 메시지(alert)를 표시하려면, 버튼이 비활성화되지 않고 대신 클릭 이벤트에서 조건을 확인해야 한다고한다.

문제 원인은 이전에 추가했던 disabled 속성이었다

현재 제출 버튼에 disabled={!reason || (reason === "기타" && !customReason.trim())}가 설정되어 있다.
이 조건에 따라 reason이 비어 있거나, "기타"를 선택했지만 customReason이 비어 있으면 버튼이 비활성화된다.
그러나 비활성화된 버튼은 클릭 이벤트를 트리거하지 않으므로 alert가 표시되지 않는다. 그래서 alert가 뜨지 않았다

버튼이 항상 활성화되어 있어야 하고, 클릭 시 조건을 확인하여 alert를 표시하거나 제출 동작을 수행하도록 수정해야한다.

해결 방법
1. disabled 속성을 제거
disabled 속성을 제거하고, 클릭 이벤트에서 조건을 확인하도록 수정한다.

  1. onClick 핸들러에서 조건 확인
    onClick 핸들러에서 reason과 customReason 값을 확인하여 경고 메시지를 표시하거나 제출 동작을 수행한다.
  <Button
            variant="danger"
            //disabled={!reason || (reason === "기타" && !customReason.trim())} // 사유가 없거나 기타 사유가 비어 있으면 비활성화
            onClick={() => {
              if (!reason) {
                alert("사유를 선택해주세요,");
                return;
              }
              // 기타 사유가 비어 있을 경우 경고창 표시
              if (reason === "기타" && !customReason.trim()) {
                alert("기타 사유를 입력해주세요.");
                return;
              }
              const finalReason = reason === "기타" ? customReason : reason;
              console.log(
                `리뷰 ID ${selectedReviewId} 삭제 요청 사유: ${finalReason}`
              );
              setIsModalOpen(false); // 모달 닫기
            }}
          >
            제출
          </Button>

비활성화 코드를 삭제한다.
disabled={!reason || (reason === "기타" && !customReason.trim())}

잘 동작한다.

신규리뷰 기본 상태를 published '승인'으로 설정하기

  {review.status === "published"
                        ? "승인됨"
                        : review.status === "rejected"
                        ? "거부됨"
                        : "대기중"}

지금까지 admin페이지에서 보이는 모든 신규리뷰들의 상태가 대기중이었는데, 승인으로 수정하였다. 즉, 리뷰를 등록하면 일괄 승인처리되고, 만약 매장관리자가 부적절한 리뷰라고 판단되어, 플랫폼운영자에게 삭제요청클릭해서 사유와함께 제출하기를 누르면 그때 상태가 대기중으로 바뀌도록한다.

newReviewData 속성에 ststus를 추가하고, 그 값으로 published를 기본값으로 추가한다. 그 전의 리뷰 상태가 모두 관리자페이지에서는 등록됨과 동시에 대기중으로 상태가 정해졌었는데, 이는 status에 대한 값이 아예 없었기 때문에 위 코드에 의해서 모든 신규리뷰는 대기중이었던 것.

 const newReviewData = {
      id: Date.now(),
      shopId: Number(shopId),
      shopName: shop?.name,
      content: newReview,
      rating: rating,
      date: new Date().toISOString(),
      authorId: currentUser?.id,
      status: "published",
    };

예를들어, 아래의 리뷰가 등록되어, 승인처리되었던 것이,

리뷰 삭제 요청으로 제출되면

상태가 대기로 변경된다.

// 리뷰 상태를 "pending"으로 업데이트
const updatedReviews = reviews.map((review) =>
review.id === selectedReviewId
? { ...review, status: "pending", deleteReason: finalReason }
: review
);
// 상태가 업데이트 되었으므로, 로컬 스토리지에 저장
localStorage.setItem("reviews", JSON.stringify(updatedReviews));
setReviews(updatedReviews);

 <Button
            variant="danger"
            // disabled={!reason || (reason === "기타" && !customReason.trim())} // 사유가 없거나 기타 사유가 비어 있으면 비활성화
            onClick={() => {
              if (!reason) {
                alert("사유를 선택해주세요,");
                return;
              }
              // 기타 사유가 비어 있을 경우 경고창 표시
              if (reason === "기타" && !customReason.trim()) {
                alert("기타 사유를 입력해주세요.");
                return;
              }
              const finalReason = reason === "기타" ? customReason : reason;

              // 리뷰 상태를 "pending"으로 업데이트
              const updatedReviews = reviews.map((review) =>
                review.id === selectedReviewId
                  ? { ...review, status: "pending", deleteReason: finalReason }
                  : review
              );

              // 상태가 업데이트 되었으므로, 로컬 스토리지에 저장장
              localStorage.setItem("reviews", JSON.stringify(updatedReviews));
              setReviews(updatedReviews);

              console.log(
                `리뷰 ID ${selectedReviewId} 삭제 요청 사유: ${finalReason}`
              );
              setIsModalOpen(false); // 모달 닫기
            }}
          >
            제출
          </Button>

admin에서 pending 된 것들은 매장리뷰에서 보이지 않도록 처리


망고케잌 이라는내용의 리뷰의 상태가 pending이라, 리뷰페이지에서도 보이지 않도록 해두었다.

published 상태인 것만 필터링해서 보여준다.

filter 메서드

reviews.filter((review) => review.status === "published")를 사용하여 status가 "published"인 리뷰만 렌더링한다.

결과적으로 status가 "pending" 또는 "rejected"인 리뷰는 리스트에서 보이지 않는다.

profile
바리바리 개바리 🌼

0개의 댓글