Project/SeSAC 2차 팀 프로젝트

[새싹x코딩온] 웹 개발자 부트캠프 과정 2차 팀프로젝트 회고 #4 | 관리자 페이지

다니니니 2024. 9. 22. 02:04
728x90

 

 

이번 프로젝트는 중고 거래 플랫폼인 만큼 그를 관리해주는 관리자의 역할이 필요했다.

관리자로서 일반 회원과 판매자로 등록된 회원을 관리할 수 있도록, 그리고 그들 사이의 거래된 내역들을 볼 수 있도록 기획하고 설계했다.

 

데이터를 보여주는 방식은 테이블 방식을 택했다.

테이블은 행과 열로 데이터를 체계적으로 배열할 수 있어서 보여주고자 하는 데이터를 명확하게 시각적으로 보여줄 수 있고, 각 항목이 잘 정리되어 있어 한눈에 정보를 파악하기 용이하기 때문이다.

1. 주요 기능

1. 전체 회원 목록 조회

관리자는 가입된 모든 회원의 목록을 조회할 수 있다. 

 

 

회원 아이디 및 닉네임으로 검색도 가능하다.

 

데이터는 axios 를 통해 서버로부터 비동기적으로 받아오고 그 데이터를 렌더링 하는 식으로 구성했다.

 

검색 기능은 입력한 검색어를 기반으로 회원 목록을 필터링할 수 있는 기능을 구현했다.

여기서 useState 훅을 사용하여 데이터를 동적으로 보여주게했다.

  // 유저 검색
  const searchUser = () => {
    const keyword = searchRef.current.value.trim();
    if (keyword === '') return showAlert('warning', '검색어를 입력해주세요');
    const result = userList.filter((user) => user[select].includes(keyword));
    setUserList(result);
    setUserCount(result.length);
  };

 

 

2. 판매자 목록 및 신고된 글 관리

관리자는 판매자로 등록된 회원 목록을 조회할 수 있다 또한, 해당 판매자들이 작성한 글의 신고된 내역을 확인할 수 있다.

판매자의 아이디나 판매자명을 누르면 해당 판매자의 신고된 내역을 확인할 수 있도록 했다.

판매자의 sellId 즉 DB의 판매자 테이블의 pk 값을 파라미터로 전달하고, 신고된 내역을 받아올 수 있도록 했다.

신고된 내역이 하나라도 있으면 useNavigate를 이용해서 해당 신고내역 리스트 url로 보내고 신고된 내역이 없으면 신고된 내역이 없다는 문구를 띄워주었다.

 

  const fetchComplaintList = async (e, sellerId, userId) => {
    e.preventDefault();
    try {
      const res = await getComplaint(sellerId);
      if (res.data.length < 1) {
        await simpleAlert('info', '신고된 내역이 없습니다.');
        return;
      }
      navigate(`/admin/complaint/${sellerId}`, { state: { userId } });
    } catch (err) {
      console.error(err);
      await simpleAlert('info', '신고된 내역이 없습니다.');
    }
  };

useNavigate로 userId까지 보내주는 이유는 신고된 내역글에서 블랙리스트 추가 기능이 있기 때문이다.

블랙리스트 추가는 유저 테이블의 기본키인 userId가 필요하므로, 해당 정보를 함께 전달했다. 

 

3. 블랙리스트 관리

신고된 글에 따라 판매자를 블랙리스트에 추가할 수 있다. 블랙리스트로 추가된 판매자의 글은 삭제된다.(만약 판매자가 올린 상품을 구매했을 경우, 수령 전이라면 환불 처리됨). 블랙리스트에 오른 판매자는 판매글을 작성하지 못한다.

4. 거래내역조회

회원들의 거래내역을 조회해서 판매 활동 및 거래 이력을 파악할 수 있다.

상품명과 판매자명, 구매자의 닉네임, 입금내역, 출금내역, 거래된 날짜등을 보여줬다.

 

 

추가로 관리자 페이지에서 일별 총 입금 금액과 총 출금 금액을 확인할 수 있다. 

 

선택한 날짜의 입금 및 출금 금액을 계산하는 함수를 구현했다. 각 함수는 서버로부터 받아온 거래 내역 정보 배열을 순회하면서 특정 날짜와 일치하는 금액을 합산한다.

  // 날짜별로 입금내역 합산
  const sumDeposit = (data, date) => {
    const deposit = data.reduce((total, entry) => {
      if (entry.deposit !== null) {
        const entryDate = timeSetting(entry.createdAt);
        if (entryDate === date) {
          total += entry.deposit;
        }
      }
      return total;
    }, 0);
    return deposit;
  };

  // 날짜별로 출금내역 합산(환불은 제외)
  const sumWithdraw = (data, date) => {
    const withdraw = data.reduce((total, entry) => {
      if (entry.withdraw !== null && entry.logStatus !== '환불') {
        const entryDate = timeSetting(entry.createdAt);
        if (entryDate === date) {
          total += entry.withdraw;
        }
      }
      return total;
    }, 0);
    return withdraw;
  };

 

그리고 이를 위해 달력을 구현했다.

달력은 이전 프로젝트에서 만든 것을 리액트로 맞게 리팩토링했다. 

const thisMonth = new Date().getMonth() + 1;
const thisYear = new Date().getFullYear();
  
const [totalDeposit, setTotalDeposit] = useState(0);
const [totalWithDraw, setTotalWithDraw] = useState(0);
const [loading, setLoading] = useState(false);
const [month, setMonth] = useState(thisMonth);
const [year, setYear] = useState(thisYear);
  
useEffect(() => {
const today = timeSetting(new Date());

if (loading) {
  // 오늘 날짜 총 입금
  const todayDeposit = sumDeposit(orderlogs, today);
  setTotalDeposit(todayDeposit);
  // 오늘 날짜 총 출금
  const todayWithdraw = sumWithdraw(orderlogs, today);
  setTotalWithDraw(todayWithdraw);

  const dates = document.querySelectorAll('.calendar .date');
  dates.forEach((ele) => {
    ele.onclick = () => {
      const selectDate = ele.getAttribute('data-date');
      const selectedDate = new Date(selectDate);
      setPickday(showDay(selectedDate));
      // 선택한 날짜의 총 입/출금 금액 보여주기
      const selectedDateDeposit = sumDeposit(orderlogs, selectDate);
      const selectedDateWithdraw = sumWithdraw(orderlogs, selectDate);
      setTotalDeposit(selectedDateDeposit);
      setTotalWithDraw(selectedDateWithdraw);
    };
  });
}
}, [loading, orderlogs, month, year]);

// 달력 만드는 함수
  const generateCalendarDates = () => {
    const thisDate = new Date(year, month - 1);
    const today = new Date();

    const prevLast = new Date(thisDate.getFullYear(), thisDate.getMonth(), 0);
    const prevDate = prevLast.getDate();
    const prevDay = prevLast.getDay();

    const thisFirst = new Date(thisDate.getFullYear(), thisDate.getMonth(), 1);
    const firstDay = thisFirst.getDay();

    const thisLast = new Date(
      thisDate.getFullYear(),
      thisDate.getMonth() + 1,
      0,
    );
    const endDate = thisLast.getDate();

    let dateElements = [];

    // 이전달 날짜
    if (firstDay !== 0) {
      for (let i = 0; i < firstDay; i++) {
        dateElements.push(
          <div
            key={`prev-${prevDate - i}`}
            className="date"
            style={{ color: '#ccc' }}
          >
            {prevDate - i}
          </div>,
        );
      }
    }

    // 이번달 날짜
    for (let i = 1; i <= endDate; i++) {
      const dateString = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
      const isToday =
        today.getDate() === i &&
        today.getMonth() === thisDate.getMonth() &&
        today.getFullYear() === thisDate.getFullYear();
      dateElements.push(
        <div key={i} className={`date`} data-date={dateString}>
          {i}
          {isToday ? <span className="today"></span> : ''}
        </div>,
      );
    }

    return dateElements;
  };
  
    // 이전달 달력으로 이동
  const handlePrevMonth = () => {
    const dates = datesRef.current;
    setMonth((prev) => {
      if (prev === 1) {
        setYear((prevYear) => prevYear - 1);
        return 12;
      }
      return prev - 1;
    });
    const dateElements = dates.querySelectorAll('.date');
    dateElements.forEach((ele) => {
      ele.onclick = () => {
        setSelectedDate(ele.getAttribute('data-date'));
      };
    });
  };

  // 다음달 달력으로 이동
  const handleNextMonth = () => {
    const dates = datesRef.current;
    setMonth((prev) => {
      if (prev === 12) {
        setYear((prevYear) => prevYear + 1);
        return 1;
      }
      return prev + 1;
    });
    const dateElements = dates.querySelectorAll('.date');
    dateElements.forEach((ele) => {
      ele.onclick = () => {
        setSelectedDate(ele.getAttribute('data-date'));
      };
    });
  };
  
  return (
  <div className="admin-calendar">
  <div className="calendar">
    <div className="calendar-header">
      <button className="prev month-btn" onClick={handlePrevMonth}>
        <FontAwesomeIcon icon={faChevronLeft} />
      </button>
      <h2 className="calendar-title">
        <div className="year-title">{year}</div>
        <div className="month-title">{month}</div>
      </h2>
      <button className="next month-btn" onClick={handleNextMonth}>
        <FontAwesomeIcon icon={faChevronRight} />
      </button>
    </div>
    <section className="cal-wrap">
      <div className="week">
        <div className="day">SUN</div>
        <div className="day">MON</div>
        <div className="day">TUE</div>
        <div className="day">WED</div>
        <div className="day">THU</div>
        <div className="day">FRI</div>
        <div className="day">SAT</div>
      </div>
      <div className="dates" ref={datesRef}>
        {generateCalendarDates()}
      </div>
    </section>
  </div>
</div>
)

 

 

이전에 바닐라 자바스크립트로 구현한 달력의 문제점은 이전 달이나 다음 달로 이동하면 날짜 데이터를 제대로 읽어올 수 없다는 것이었다. 이를 해결하기 위해 리액트 스타일에 맞게 달력 코드를 리팩토링했다.

useEffect를 활용하여 의존성배열에 month와 year 정보를 포함시켜서 달이나 년도가 변경될 때 재렌더링되도록 구현했다.

 

 

2. 후기

관리자 페이지를 구현하면서 어려웠던 점은 "관리자가 이 행동을 하는 것이 맞는가?" 하는 의문점이 드는 것이었다.

쇼핑몰 관리자로서의 경험이 없다보니... 기획이나 설계단계에서 관리자라면 이럴것이다. 라고 추상적인 가정만으로 진행했다. 그러다보니 아쉬움도 많이 남는 페이지다.

또한 데이터가 더 쌓였더라면 월별 통계나 차트를 통해 거래내역을 시각적으로 표현하는 기능을 추가할 수도 있었겠지만 3~4주라는 짧은 시간동안 개발을 하다보니 그 기능은 놓친거 같아서 아쉬움이 많이 남는다..

초기에는 단순히 테이블 태그를 이용해서 보여줬는데, 프로젝트 발표 후 리더님 피드백을 들어보니 리액트 테이블이라는 것도 있다고 들어서 그 점을 미리 알고 활용했더라면 더욱 효과적인 결과를 얻을 수 있었을 것이라는 생각도 든다.

(sheet.js 나 chart.js 를 활용해보지 못했다는 아쉬움이 크다..)

 

다음에 비슷한 프로젝트를 하게 되거나 리팩토링을 하게 된다면 리액트 테이블이나 chart.js, sheet.js 를 통해서 더욱 시각적으로 유저 친화적인 것을 활용해봐야겠다는 다짐을 했다.

728x90