웹알못 dJango로 홈페이지 만들기(관리자페이지) [4]

     

 


목차

2019/05/16 - [Study/python] - 웹알못 dJango로 홈페이지 만들기(관리자페이지) [1]

- 개발환경, 장고설치, 첫화면 만들기, 수정/삭제기능 만들기

2019/05/16 - [Study/python] - 웹알못 dJango로 홈페이지 만들기(관리자페이지) [2]

- 페이징 처리, 검색 기능, 추가 기능

2019/05/17 - [Study/python] - 웹알못 dJango로 홈페이지 만들기(관리자페이지) [3]

- 메뉴바, 로그 페이지 추가

2019/05/20 - [Study/python] - 웹알못 dJango로 홈페이지 만들기(관리자페이지) [4]

- 일별, 월별 데이터 뽑기, 기간 필터링, 차트그리기

git - https://github.com/tkdlek11112/simple_dashboard_python


▷ 일별, 월별 데이터 뽑기

이제 로그데이터를 바탕으로 통계 페이지를 만들 예정입니다. 가장 간단하게 일별 통계를 먼저 만들껀데... dJango에서 쿼리 셋으로 날짜별 count를 출력할 수 있을지 모르겠네요. 일단 구글링 꼬~

구글링 결과 annotate를 사용해서 group by 효과를 볼 수 있는 쿼리 셋을 만들 수 있다고 합니다. 대략적으로 아래와 같이 만들면...

# adminpage/views.py
from django.db.models.functions import TruncMonth, TruncDate
def statisticslogs(request):
    stat_type = request.GET.get('stat_type')

    if stat_type == 'M':
        stats = Log.objects \
            .annotate(stat_date=TruncMonth('log_date')) \
            .values('stat_date') \
            .annotate(stat_count=Count('log_userid')
                      ).values('stat_date', 'stat_count')
    else:
        stats = Log.objects \
            .annotate(stat_date=TruncDate('log_date')) \
            .values('stat_date') \
            .annotate(stat_count=Count('log_userid')
                      ).values('stat_date', 'stat_count')

    context = {'stats': stats}
    return render(request, 'adminpage/statistics_logs.html', context)

stat_type은 M일 경우 월별, D일 경우 일별 통계를 뽑기 위해 구분했습니다. 쿼리 셋을 만들 때, annotate(stat_date=TruncMonth('log_date'))를 만들어서 stat_date로 group by효과를 주었고, TruncMonth는 날짜에서 월만 빼는 dJango 메서드입니다. html은 statistics_logs.html로 만들었어요. 넘어가는 값이 현재는 stat_date와 stat_count 두 개입니다.

{# template/adminpage/statistics_logs.html #}
    <table class = "tg">
    <colgroup>
        <col width="50%">
        <col width="50%">
    </colgroup>
        <tr>
            <th class="tg-21xh"> 기준 </th>
            <th class="tg-21xh"> 카운트 </th>
        </tr>
        {%  for stat in stats %}
        <tr>
            <td>{{ stat.stat_date }}</td>
            <td>{{ stat.stat_count }}</td>
        </tr>
        {% endfor %}
    </table>

일별 통게 화면

요런 식으로 통계치가 나옵니다. 생각보다 쉽죠? 자세한 쿼리 셋 사용법은 구글링 꼬~

▷ 날짜로 필터링 걸기

첫 번째로 만들었던 질문 답변 리스트 데이터는 날짜 필드가 없어서 상관없는데, 로그 리스트 보는 페이지와 통계에는 범위를 넣을 수 있는 위젯이 필요할 것 같습니다. 특정 범위로 검색하는 일이 많을 것 같아서요. (+ 추가로 통계에는 일/월 선택창도 필요할 듯)

구글링으로 부트스트랩 날짜입력 폼을 검색해서 아래와 같이 만듭니다.

{# template/adminpage/statistics_logs #}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="/static/jquery-3.2.1.min.js"></script>
    <link rel="stylesheet" href="/static/css/bootstrap.css">

    <script type="text/javascript" src="/static/bootstrap.js"></script>
    <script type="text/javascript" src="/static/jquery.bootstrap.modal.forms.js"></script>

    <link href="/static/css/bootstrap-datetimepicker.min.css" rel="stylesheet" />
    <script type="text/javascript" src="/static/moment-with-locales.min.js"></script>
    <script type="text/javascript" src="/static/bootstrap-datetimepicker.min.js"></script>


</head>
<body>
<style type="text/css">
.tg  {border-collapse:collapse;border-spacing:0;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg .tg-21xh{font-weight:bold;background-color:#34cdf9;color:#333333;border-color:inherit;text-align:left;vertical-align:top}
.tg .tg-0pky{border-color:inherit;text-align:left;vertical-align:top}
.content_wrap {width: 80%; margin: 0 auto;}
.pagination  {text-align: center; width: 100%;}
</style>


    <div class="modal fade" tabindex="-1" role="dialog" id="modal">
        <div class="modal-dialog" role="document">
            <div class="modal-content">

            </div>
        </div>
    </div>

{% include "adminpage/menu.html" %}

<div class="content_wrap">
<div style="position: relative">
    <input id="fromDate" type="text" >
    <input id="toDate" type="text">
</div>
    <table class = "tg">
    <colgroup>
        <col width="50%">
        <col width="50%">
    </colgroup>
        <tr>
            <th class="tg-21xh"> 기준 </th>
            <th class="tg-21xh"> 카운트 </th>
        </tr>
        {%  for stat in stats %}
        <tr>
            <td>{{ stat.stat_date }}</td>
            <td>{{ stat.stat_count }}</td>
        </tr>
        {% endfor %}
    </table>
</div>
</body>
    <script type="text/javascript">
    $(function(){
        $('#fromDate').datetimepicker({
        });

        $('#toDate').datetimepicker({
        });
    });

</script>

</html>

키포인트는 input을 만들고. datetimepicker를 달아서 클릭했을 때 날짜를 선택하는 위젯을 띄우는 겁니다. datetimepicker는 부트스트랩에서 제공하는 위젯인데요, 사용하기 위해서 js/css 파일을 받아서 선언을 해줘야 합니다.

    <link href="/static/css/bootstrap-datetimepicker.min.css" rel="stylesheet" />
    <script type="text/javascript" src="/static/moment-with-locales.min.js"></script>
    <script type="text/javascript" src="/static/bootstrap-datetimepicker.min.js"></script>

요 3줄이 핵심입니다. locales.js는 호출 안 했더니 에러가 나서 추가해줬습니다. 그리고 또 바뀐 점이 있는데, 기존에는 css, js파일들을 /static/ 경로에 몰아서 저장해놨었는데, /static/css, /static/fonts, /static/으로 나누었습니다. 나누지 않았더니 css에서 fonts를 호출할 때 경로 에러가 나더라고요. bootstrap.css 소스 까 보면 폰트 경로가 ../fonts/..로 되어있어서 상위 경로에 fonts폴더를 만들기 위해 css폴더 안으로 다 밀어 넣었습니다.

경로를 분리해줬다.

참고로 저 fonts파일 안에 있는 것도 다운로드하여줘야 자잘한 에러가 발생하지 않더군요. 아무튼 이걸 적용하면 아래와 같이 날짜를 선택하는 위젯이 뜹니다.

datetimepicker

근데 만들고 생각해보니 시간 단위로 검색이 필요한가...?라는 생각 때문에 datetimepicker에서 datepicker로 바꿨습니다. ㅋㅋㅋㅋ js/css파일 다시 받아서 고대로 하시면 됩니다. 그리고 기간으로 검색하고 싶지 않고 전체로 검색하는 경우도 있기 때문에 라디오 버튼을 추가해 전체/기간을 선택할 수 있게 만들고, 전체를 눌렀을 경우 datepicker를 초기화하고 선택할 수 없게 만듭니다. 이래야 혼선을 줄일 수 있으니까요. 그리고 아래 몇 가지 편의 기능들을 추가합니다.

1. 이전에 선택, 입력했던 데이터 그대로 표기

2. 전체 선택 시 기간선택 불가. 값 초기화

3. 기간 선택시 해당 기간으로 통계

4. 일별/월별 구분하여 통계

{# template/adminpage/statistics_logs.html #}
<form action="/statistics_logs/" method="get">
    <fieldset >
        <select id="stat_type" name="stat_type">
            <option value="D" {% if stat_type != 'M' %} selected {% endif %}> 일별 </option>
            <option value="M" {% if stat_type == 'M' %} selected {% endif %}> 월별 </option>
        </select>
        <div class="radio" style="display: inline">
            <label> <input type="radio" name="optionRadios" id="optionsRadios1" value="total"
                           {% if optionRadios != 'period' %} checked {% endif %}/> 전체 </label>
        </div>
        <div class="radio" style="display: inline">
            <label> <input type="radio" name="optionRadios" id="optionsRadios2" value="period"
            {% if optionRadios == 'period' %} checked {% endif %}/> 기간 : </label>
            <input id="fromDate" data-provide="datepicker" data-date-format="yyyy-mm-dd" name="from_date" value = {{ from_date }}>
            <label> ~ </label>
            <input id="toDate" data-provide="datepicker" data-date-format="yyyy-mm-dd" name="to_date" value = {{ to_date }}>
        </div>
        <button type="submit">조회</button>

    </fieldset>
</form>
#adminpage/views.py
def statisticslogs(request):
    stat_type = request.GET.get('stat_type')
    stat_gbn = request.GET.get('optionRadios')
    to_date = request.GET.get('to_date')
    from_date = request.GET.get('from_date')
    if stat_type == 'M':
        if stat_gbn == 'period':
            stats = Log.objects \
                .filter(log_date__range=[from_date, to_date]) \
                .annotate(stat_date=TruncMonth('log_date')) \
                .values('stat_date') \
                .annotate(stat_count=Count('log_userid')
                          ).values('stat_date', 'stat_count')
        else:
            stats = Log.objects \
                .annotate(stat_date=TruncMonth('log_date')) \
                .values('stat_date') \
                .annotate(stat_count=Count('log_userid')
                          ).values('stat_date', 'stat_count')
    else:
        if stat_gbn == 'period':
            stats = Log.objects \
                .filter(log_date__range=[from_date, to_date])\
                .annotate(stat_date=TruncDate('log_date')) \
                .values('stat_date') \
                .annotate(stat_count=Count('log_userid')
                          ).values('stat_date', 'stat_count')
        else:
            stats = Log.objects \
                .annotate(stat_date=TruncDate('log_date')) \
                .values('stat_date') \
                .annotate(stat_count=Count('log_userid')
                          ).values('stat_date', 'stat_count')

    context = {'stats': stats,
               'stat_type': stat_type,
               'optionRadios': stat_gbn,
               'to_date': to_date,
               'from_date': from_date}
    return render(request, 'adminpage/statistics_logs.html', context)

html과 views.py를 위와 같이 수정합니다. views.py를 보면 if문이 상당히 많은데, 조건을 따져야 할게 많아서 어쩔 수 없네요 ㅎㅎ 동적으로 하는 방법이 있을까? 잠깐 고민했지만 역시 단순한 게 최고 져~

기간으로 필터링하는 것은 object.filter(log_date__range=[])를 사용했는데, 기본으로 들어가야 하는 형태가 yyyy-mm-dd형태여서 어쩔 수 없이 datepicker form에서 format을 yyyy-mm-dd형태로 변경했습니다.

필터링 작동 확인

같은 기능을 LOG데이터 화면에도 넣어줍시다~!

여기에도 날짜 필터링 넣고 views.py도 수정하는거 잊지마세요~

▷ 차트 넣기

이제 대망의 차트 그리기입니다. 아~~ 주 다행히도 오픈소스로 된 차트 라이브러리들이 많더라고요. 그중에서 apexchart라는 라이브러리를 사용하기로 했습니다. 사이트는 아래!

https://apexcharts.com

[

ApexCharts.js – Open-Source HTML5JavaScript Charts

ApexCharts.js - An open-source HTML5 JavaScript charting library that helps developers to create responsive & interactive JS charts for web pages.

apexcharts.com

](https://apexcharts.com/)

여러 가지 차트가 있는데 여기서 막대차트(column)를 이용할 계획입니다. 사용법은 그냥 다운로드한 다음에 예제를 복붙 해서 적용하면 끝~! 저는 아래와 같이 적용했습니다.

# template/adminpage/column-with-data-labes.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Column with Data Labels</title>


    <link href="/static/css/styles.css" rel="stylesheet" />

    <style>
        #chart {
            max-width: 650px;
            margin: 35px auto;
        }
    </style>
</head>

<body>
    <div id="chart">

    </div>


    <script src="https://cdn.jsdelivr.net/npm/apexcharts@latest"></script>

    <script>
        var aa = "{{ date_list }}"
        var options = {
            chart: {
                height: 350,
                type: 'bar',
            },
            plotOptions: {
                bar: {
                    dataLabels: {
                        position: 'bottom', // top, center, bottom
                    },
                }
            },
            dataLabels: {
                enabled: true,
                formatter: function (val) {
                    return val ;
                },
                offsetY: -20,
                style: {
                    fontSize: '22px',
                    colors: ["#304758"]
                }
            },
            series: [{
                name: 'Chat',
                data: {{ date_count | safe  }}
            }],
            xaxis: {
                categories: {{ date_list | safe }},
                position: 'top',
                labels: {
                    offsetY: -18,

                },
                axisBorder: {
                    show: false
                },
                axisTicks: {
                    show: false
                },
                crosshairs: {
                    fill: {
                        type: 'gradient',
                        gradient: {
                            colorFrom: '#D8E3F0',
                            colorTo: '#BED1E6',
                            stops: [0, 100],
                            opacityFrom: 0.4,
                            opacityTo: 0.5,
                        }
                    }
                },
                tooltip: {
                    enabled: true,
                    offsetY: -35,

                }
            },
            fill: {
                gradient: {
                    shade: 'light',
                    type: "horizontal",
                    shadeIntensity: 0.25,
                    gradientToColors: undefined,
                    inverseColors: true,
                    opacityFrom: 1,
                    opacityTo: 1,
                    stops: [50, 0, 100, 100]
                },
            },
            yaxis: {
                axisBorder: {
                    show: false
                },
                axisTicks: {
                    show: false,
                },
                labels: {
                    show: false,
                    formatter: function (val) {
                        return val ;
                    }
                }

            },
            title: {
                text: '키봇 사용 현황',
                floating: true,
                offsetY: 320,
                align: 'center',
                style: {
                    color: '#444'
                }
            },
        }

        var chart = new ApexCharts(
            document.querySelector("#chart"),
            options
        );

        chart.render();


    </script>
</body>

# adminpage/views.py

def chart(request):
    stats = Log.objects \
        .annotate(stat_date=TruncDate('log_date')) \
        .values('stat_date') \
        .annotate(stat_count=Count('log_userid')
                  ).values('stat_date', 'stat_count')

    date_list = [];
    date_count = [];
    for stat in stats:
        print(stat['stat_date'])
        date_list.append(str(stat['stat_date']))
        date_count.append(str(stat['stat_count']))
    print(date_list)
    context = {'stats': stats, 'date_list': json.dumps(date_list), 'date_count' : json.dumps(date_count)}
    return render(request, 'adminpage/column-with-data-labels.html', context)

보시면 아시겠지만 일단 테스트하기 위해 새로운 html을 만들었습니다. 그리고 views.py에서 데이터를 뽑아서 전달하는데, 중요한 점은 json.dumps()메서드를 사용했다는 것입니다. 일반적으로 그냥 보냈더니 html에 있는 javascript에서 변수를 읽어오기 힘들더라고요. 일단 그래프를 그리기 위해서는 리스트 형태의 데이터가 필요하기 때문에 for문을 돌려서 list를 만든 다음, json.dumps()를 이용해 json 형태로 날립니다. 그리고 html에서 받을 때는 {{ var_name | safe }} 이렇게 safe를 안 해주면 특수문자가 깨지니까 체크해주세요~ 화면은 아래와 같이 나옵니다.

 

통계차트

 

단시간 안에 그럴듯하게 나오죠? 이제 통계 페이지 표 옆에 붙이면 완성입니다!

 

오옷..!

 

이제 웬만한 기능은 다 만들었네요 ㅎㅎ 자잘하게 요청사항만 들어오면 수정하면 될 것 같습니다. 처음이라 막막했던 웹 알못에 관리자 페이지 만들기 성공! 

 

하다가 자잘한 팁들 계속해서 올리도록 하겠습니다~ ㅎㅎ

 

To be continue

반응형

댓글

Designed by JB FACTORY