본문 바로가기
BackEnd/Project

[PT Manager] Ch03. Web 관리자 통계 페이지

by 개발 Blog 2024. 8. 28.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

이번 시간에는 웹 관리자 페이지에서 관리자가 통계를 조회할 수 있는 세 번째 페이지를 만들어본다. 이전에 배치 작업을 통해 통계 데이터를 생성했고, CSV 파일로 데이터를 저장하기도 했다. 그러나 관리자 페이지에서 차트로 통계를 시각화하면 더 쉽게 데이터를 분석하고 활용할 수 있다.

통계 조회 페이지 개요

이번에 구현할 통계 조회 페이지는 statistic 데이터를 조회하여 일별 출석 및 취소 횟수를 차트로 시각화하는 기능을 제공한다. 이 페이지는 이전에 구현한 다른 페이지들에 비해 비교적 간단하다. 주로 통계 데이터만 조회하며, 단일 통계 조회 기능만 구현하면 되기 때문이다.

index.html과 차트 구현

index.html은 관리자 대시보드의 메인 페이지로, 다양한 통계 정보를 시각화하여 보여주는 역할을 한다. 이 페이지에는 일별 출석 및 취소 횟수를 시각화한 차트가 포함되어 있다.

 

1. 기본 레이아웃

페이지 상단에는 대시보드 타이틀과 간단한 다운로드 버튼이 배치되어 있으며, 여러 통계 항목들이 카드 형식으로 나열되어 있다. 각 카드에는 활성화된 회원 수, 월간 소득, 진행 중인 태스크, 그리고 오늘의 수업 수와 같은 주요 정보가 표시된다.

 

2. 일별 출석 및 취소 차트

페이지 하단에는 일별 출석 및 취소 횟수를 시각화한 라인 차트가 포함되어 있다. 이 차트는 Chart.js 라이브러리를 사용하여 구현되었으며, 데이터는 서버에서 ChartData 객체로 전달된다. ChartData 객체는 차트의 X축에 해당하는 날짜 라벨(labels), 출석 횟수(attendedCounts), 취소 횟수(cancelledCounts) 리스트를 포함한다.

<div class="chart-area">
    <canvas id="myAreaChart"></canvas>
</div>
  • 위와 같은 코드 블록은 차트를 렌더링 할 캔버스 요소를 정의하고 있으며, 차트는 chart-area.js에서 생성된다.

3. chartData 전달과 차트 렌더링

index.html 파일의 마지막 부분에는 서버에서 전달된 chartData를 클라이언트 측 자바스크립트 변수로 받아오는 스크립트와, 이를 이용해 차트를 렌더링 하는 JavaScript 파일(chart-area.js)을 로드하는 코드가 포함되어 있다.

<script th:inline="javascript">
    /*<![CDATA[*/
    let chartData = /*[[${chartData}]]*/ "";
    /*]]>*/
</script>
<script src="/admin/js/chart-area.js"></script>
  • chartData 변수 설정: 이 스크립트 블록은 서버에서 전달된 chartData 객체를 자바스크립트 변수로 받아와서 클라이언트 측에서 사용할 수 있게 한다. 이 변수에는 차트를 그리기 위해 필요한 데이터(날짜 라벨, 출석 횟수, 취소 횟수 등)가 포함되어 있다.
  • chart-area.js 로드: 이 스크립트는 chart-area.js 파일을 로드하여, 해당 데이터로 차트를 생성한다. chart-area.js 파일 내에서는 Chart.js 라이브러리를 사용하여 실제로 라인 차트를 그리게 된다.

이 과정은 서버에서 처리된 데이터를 클라이언트 측에서 시각적으로 표시하는 중요한 단계이다. 관리자는 이 차트를 통해 쉽게 일별 출석 및 취소 현황을 분석할 수 있으며, 데이터가 시각화되어 훨씬 더 직관적인 인사이트를 얻을 수 있다.

 

4. chart-area.js 파일

chart-area.js 파일은 Chart.js를 사용하여 차트를 그리는 JavaScript 파일이다. 이 파일은 index.html에서 <script> 태그를 통해 포함되며, 차트를 그리기 위해 ChartData 객체에 담긴 데이터를 사용한다.

차트를 구현하기 위해 Chart.js 라이브러리를 사용한다. Chart.js를 통해 라인 차트를 생성하고, 출석 횟수와 취소 횟수를 각각 다른 색상으로 구분하여 표시한다. 데이터는 컨트롤러에서 전달받은 ChartData 객체를 사용하여 렌더링 된다.

chart-area.js 파일은 차트의 기본 설정과 스타일링, 숫자 포맷팅 등의 기능도 포함하고 있다. 이를 통해 사용자에게 일관된 UI를 제공하며, 데이터 시각화가 원활하게 이루어지도록 한다.

// Set new default font family and font color to mimic Bootstrap's default styling
Chart.defaults.global.defaultFontFamily = 'Nunito', '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
Chart.defaults.global.defaultFontColor = '#858796';

function number_format(number, decimals, dec_point, thousands_sep) {
    // *     example: number_format(1234.56, 2, ',', ' ');
    // *     return: '1 234,56'
    number = (number + '').replace(',', '').replace(' ', '');
    var n = !isFinite(+number) ? 0 : +number,
        prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
        sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
        dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
        s = '',
        toFixedFix = function (n, prec) {
            var k = Math.pow(10, prec);
            return '' + Math.round(n * k) / k;
        };
    // Fix for IE parseFloat(0.55).toFixed(0) = 0;
    s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
    if (s[0].length > 3) {
        s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
    }
    if ((s[1] || '').length < prec) {
        s[1] = s[1] || '';
        s[1] += new Array(prec - s[1].length + 1).join('0');
    }
    return s.join(dec);
}

// Area Chart Example
var ctx = document.getElementById("myAreaChart");

var myLineChart = new Chart(ctx, {
    type: 'line',
    data: {
        labels: chartData.labels,
        datasets: [{
            label: "출석 횟수",
            lineTension: 0.3,
            backgroundColor: "rgba(78, 115, 223, 0.05)",
            borderColor: "rgba(78, 115, 223, 1)",
            pointRadius: 3,
            pointBackgroundColor: "rgba(78, 115, 223, 1)",
            pointBorderColor: "rgba(78, 115, 223, 1)",
            pointHoverRadius: 3,
            pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
            pointHoverBorderColor: "rgba(78, 115, 223, 1)",
            pointHitRadius: 10,
            pointBorderWidth: 2,
            data: chartData.attendedCounts,
        }, {
            label: "취소 횟수",
            lineTension: 0.3,
            backgroundColor: "rgba(231, 74, 59, 0.05)",
            borderColor: "rgba(231, 74, 59, 1)",
            pointRadius: 3,
            pointBackgroundColor: "rgba(231, 74, 59, 1)",
            pointBorderColor: "rgba(231, 74, 59, 1)",
            pointHoverRadius: 3,
            pointHoverBackgroundColor: "rgba(231, 74, 59, 1)",
            pointHoverBorderColor: "rgba(231, 74, 59, 1)",
            pointHitRadius: 10,
            pointBorderWidth: 2,
            data: chartData.cancelledCounts
        }],
    },
    options: {
        maintainAspectRatio: false,
        layout: {
            padding: {
                left: 10,
                right: 25,
                top: 25,
                bottom: 0
            }
        },
        scales: {
            xAxes: [{
                time: {
                    unit: 'date'
                },
                gridLines: {
                    display: false,
                    drawBorder: false
                },
                ticks: {
                    maxTicksLimit: 10
                }
            }],
            yAxes: [{
                ticks: {
                    maxTicksLimit: 5,
                    padding: 10,
                    // Include a dollar sign in the ticks
                    callback: function (value, index, values) {
                        return number_format(value);
                    }
                },
                gridLines: {
                    color: "rgb(234, 236, 244)",
                    zeroLineColor: "rgb(234, 236, 244)",
                    drawBorder: false,
                    borderDash: [2],
                    zeroLineBorderDash: [2]
                }
            }],
        },
        legend: {
            display: false
        },
        tooltips: {
            backgroundColor: "rgb(255,255,255)",
            bodyFontColor: "#858796",
            titleMarginBottom: 10,
            titleFontColor: '#6e707e',
            titleFontSize: 14,
            borderColor: '#dddfeb',
            borderWidth: 1,
            xPadding: 15,
            yPadding: 15,
            displayColors: false,
            intersect: false,
            mode: 'index',
            caretPadding: 10,
            callbacks: {
                label: function (tooltipItem, chart) {
                    var datasetLabel = chart.datasets[tooltipItem.datasetIndex].label || '';
                    return datasetLabel + ': ' + number_format(tooltipItem.yLabel);
                }
            }
        }
    }
});

백엔드 구현

1. ChartData 클래스

통계 데이터를 차트로 시각화하기 위해 ChartData 클래스를 사용한다. 이 클래스는 차트의 X축에 표시될 날짜 라벨(labels), Y축에 표시될 출석 횟수(attendedCounts), 그리고 취소 횟수(cancelledCounts)의 리스트를 담고 있다.

서비스 계층에서 ChartData 객체를 생성하고, 컨트롤러를 통해 뷰로 전달되며, 최종적으로 chart-area.js 파일에서 이 데이터를 이용해 차트를 그린다.

package com.example.pass.service.statistics;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Getter
@Setter
@ToString
public class ChartData {
    private List<String> labels;
    private List<Long> attendedCounts;
    private List<Long> cancelledCounts;

    public ChartData(List<String> labels, List<Long> attendedCounts, List<Long> cancelledCounts) {
        this.labels = labels;
        this.attendedCounts = attendedCounts;
        this.cancelledCounts = cancelledCounts;

    }

}

 

2. AdminViewController 수정

관리자 대시보드에서 통계 데이터를 조회하여 차트를 렌더링 하기 위해 컨트롤러를 수정한다. StatisticsService를 통해 통계 데이터를 가져오고, 이를 chartData 객체로 변환하여 뷰에 전달한다.

@Autowired
private StatisticsService statisticsService;

@GetMapping
public ModelAndView home(ModelAndView modelAndView, @RequestParam("to") String toString) {
    LocalDateTime to = LocalDateTimeUtils.parseDate(toString);

    modelAndView.addObject("chartData", statisticsService.makeChartData(to));
    modelAndView.setViewName("admin/index");
    return modelAndView;
}

 

3. 데이터 모델과 리포지토리

AggregatedStatistics와 StatisticsEntity 클래스는 통계 데이터를 관리하기 위한 데이터 모델이다. StatisticsRepository는 통계 데이터를 조회하고, 일별로 그룹화된 통계 데이터를 반환하는 쿼리를 제공한다.

package com.example.pass.service.statistics;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.time.LocalDateTime;

@Getter
@Setter
@ToString
@AllArgsConstructor
public class AggregatedStatistics {
    private LocalDateTime statisticsAt; // 일 단위
    private long allCount;
    private long attendedCount;
    private long cancelledCount;

    public void merge(final AggregatedStatistics statistics) {
        this.allCount += statistics.getAllCount();
        this.attendedCount += statistics.getAttendedCount();
        this.cancelledCount += statistics.getCancelledCount();

    }
}
package com.example.pass.repository.statistics;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.time.LocalDateTime;

@Getter
@Setter
@ToString
@Entity
@Table(name = "statistics")
public class StatisticsEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 DB에 위임합니다. (AUTO_INCREMENT)
    private Integer statisticsSeq;
    private LocalDateTime statisticsAt; // 일 단위

    private int allCount;
    private int attendedCount;
    private int cancelledCount;

}
package com.example.pass.repository.statistics;

import com.example.pass.service.statistics.AggregatedStatistics;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.List;

public interface StatisticsRepository extends JpaRepository<StatisticsEntity, Integer> {

    @Query(value = "SELECT new com.example.pass.service.statistics.AggregatedStatistics(s.statisticsAt, SUM(s.allCount), SUM(s.attendedCount), SUM(s.cancelledCount)) " +
            "         FROM StatisticsEntity s " +
            "        WHERE s.statisticsAt BETWEEN :from AND :to " +
            "     GROUP BY s.statisticsAt")
    List<AggregatedStatistics> findByStatisticsAtBetweenAndGroupBy(@Param("from") LocalDateTime from, @Param("to") LocalDateTime to);

}

 

4. StatisticsService

StatisticsService는 통계 데이터를 조회하고 차트에 필요한 데이터를 가공하는 서비스다. 특정 기간 동안의 출석 및 취소 횟수를 집계하여 ChartData 객체로 변환하고, 이를 통해 차트를 렌더링 할 수 있도록 한다.

package com.example.pass.service.statistics;

import com.example.pass.repository.statistics.StatisticsRepository;
import com.example.pass.util.LocalDateTimeUtils;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
public class StatisticsService {

    private final StatisticsRepository statisticsRepository;

    public StatisticsService(StatisticsRepository statisticsRepository) {
        this.statisticsRepository = statisticsRepository;
    }

    public ChartData makeChartData(final LocalDateTime to) {
        final LocalDateTime from = to.minusDays(10);

        final List<AggregatedStatistics> aggregatedStatisticsList = statisticsRepository.findByStatisticsAtBetweenAndGroupBy(from, to);
        List<String> labels = new ArrayList<>();
        List<Long> attendedCounts = new ArrayList<>();
        List<Long> cancelledCounts = new ArrayList<>();

        for (AggregatedStatistics statistics : aggregatedStatisticsList) {
            labels.add(LocalDateTimeUtils.format(statistics.getStatisticsAt(), LocalDateTimeUtils.MM_DD));
            attendedCounts.add(statistics.getAttendedCount());
            cancelledCounts.add(statistics.getCancelledCount());
        }
        return new ChartData(labels, attendedCounts, cancelledCounts);

    }
}

실행화면

아래 이미지에서 볼 수 있듯이, 통계 페이지가 제대로 구현되어 실행되고 있다. 이 페이지는 관리자가 일별 출석 및 취소 횟수를 시각적으로 확인할 수 있도록 설계되었다.

  • 대시보드 상단: 활성화된 회원 수, 소득, 태스크 진행률, 오늘의 수업 수와 같은 주요 통계 정보가 카드 형식으로 표시된다. 이 정보는 관리자에게 현재 상태에 대한 빠른 개요를 제공한다.
  • 일별 출석 및 취소 차트: 페이지 하단에 일별 출석 및 취소 횟수를 나타내는 라인 차트가 표시된다. 이 차트는 Chart.js 라이브러리를 사용하여 구현되었으며, 파란색 선은 출석 횟수를, 빨간색 선은 취소 횟수를 나타낸다. 마우스를 데이터 포인트 위에 올리면 해당 날짜의 출석 및 취소 수를 툴팁으로 확인할 수 있다.

이 실행 화면은 이전에 설명한 모든 기능이 제대로 작동하고 있음을 보여준다. 관리자는 이 차트를 통해 최근의 출석 및 취소 패턴을 빠르게 파악할 수 있으며, 이를 바탕으로 운영 전략을 세울 수 있다. 데이터가 시각적으로 표현되기 때문에, CSV 파일이나 숫자만으로는 어렵게 느껴질 수 있는 정보를 훨씬 쉽게 분석할 수 있다.

이제 3개의 주요 페이지인 사용자 이용권 조회 페이지, 관리자 이용권 등록 페이지, 관리자 통계 조회 페이지가 모두 구현이 완료되었다. 다음 시간에는 이 페이지들에 대한 기능을 mock 데이터를 이용해 테스트하는 방법을 다룰 예정이다. 이를 통해 구현된 기능이 의도한 대로 동작하는지 검증할 수 있을 것이다.