본문 바로가기

멀티캠퍼스 프로젝트형 AI 서비스 개발 5회차/프로젝트

3/14 월_인터페이스 프로젝트

728x90

오랜만!!

드디어 인터페이스 프로젝트가 마무리되었다!
2/22 화요일부터 3/9 수요일까지 16일 동안 작업하고, 3/14 오늘! 발표를 진행했다.

우리는 미리 대본을 만들어 발표부터 시연까지 영상으로 촬영함.
예방접종 현황 API랑 마지막까지 상세 페이지 웹디자인 만진 거(색깔 통일) GitHub에 올렸는데,
영상에 반영이 안 돼서 너무 아쉽다. 😂

1조 - 영화 커뮤니티 : 소셜(카카오 계정) 연동 회원가입
2조 - 맛집 커뮤니티 : Map API
3조 - 쇼핑몰 : 제품 카테고리, 장바구니, 댓글 수 순으로 제품 추천
4조 - 코로나 커뮤니티 : Map API(병원 · 약국 마킹), 예방접종 · 확진자 현황 API, 자가진단, 전문의 상주 게시판
5조 - 영화 · TV 프로그램 검색 : 장르에 따라 추천, 웹디자인
6조 - 박스오피스 : 일 · 주로 구분
7조 - 다이어리 커뮤니티 : 새 글 작성 시 지도 키워드 검색으로 위치 추가, 시연 영상 편집

아이디어 및 Data - 18점, 교과 내용 반영도 - 40점, PJT 관리 - 22점, 프레젠테이션 - 20점이 배점(총 100점)되고,
강사님 5 : 상대평가 5로 최우수상 1팀을 가린다! 두구두구두구 어느 팀이 될 것인가!!!!!

0.5점 차이로 우리가 1등ㅎㅎㅎㅎㅎㅎㅎㅎ 엌 기대 안 했는데, 팀원들 덕분!!!!!

프로젝트 때 내가 구현한 기능 정리해서 게시해야지!
다른 팀원들이 한 코드(Map API, 병원 · 약국 마킹, 게시판)도 뜯어보면서 공부해야겠다.

오늘은 수행평가 제출하고, 지난주 목요일부터 기획 · 모집 중인 스터디의 정식 첫 모임을 가질 예정이다.

할게 너무나 많은 요즘~~~ 🤯 그래도 1등 하니 기분은 좋구먼 😎

 

 


 

이번 프로젝트에서 내가 맡은 부분은 전반적인 웹디자인(HTML, CSS, JavaScript)과
메인 화면의 예방접종 및 일일 확진자 발생 현황(Open API), 자가진단 페이지 구현이다.

 

웹디자인은 수업 때 활용한 Bootstrap이 아닌,
정부의 코로나바이러스 감염증-19 사이트(http://ncov.mohw.go.kr/)를 토대로 필요한 것만 추려서 사용했다.

 

Base Template을 만들어 그 안에 CDN(Bootstrap, jQuery), icon, CSS, Javascript를 연결해주었고,
기본 메뉴를 넣어 block(html_header/body)으로 다른 페이지들에 일괄적으로 적용되도록 하였다.


프론트엔드를 심도 있게 배운 것은 아니어서,
초반에 틀을 잡고 불필요한 것들을 제외하고 기획에 맞게 재조정하는데 시간이 많이 걸렸다. 😥

각각의 엘리먼트들에 대한 속성을 알지 못하다 보니.. 하나씩 맞춰나갔다.

 

백엔드는, 공공데이터 포털의 Open API 두 가지(코로나19 예방접종 통계 데이터 조회 서비스, 보건복지부 코로나19 감염 현황)를 사용했다.

 

자동으로 당일 날짜가 request url 혹은 parameter로 들어갈 수 있도록 함수를 만들었고,

Javascript, Ajax GET 방식으로 데이터를 Json, XML 형식으로 가져와

HTML Element의 내용을 Selector(선택자)와 text(), attr(), val() 함수들을 이용해 가져온 값으로 대체해주도록 코드를 작성했다.

 

여기서 어려웠던 점은, 데이터 형식에 따라 indexing 하는 방법이 다르다는 점!
특히나 XML 형식의 데이터를 인덱싱 하는 게 생각보다 힘들었다. 쓰이는 함수가 다름! 😵

 

코로나 감염 현황은 일일 신규 확진자가 없고 일일 누적 확진자만 있어서,
당일에서 전날 누적을 빼는 것으로 당일 치를 계산했다.

 

.toString().replace() 함수로 숫자 3자리마다 쉼표를 넣고,
예방접종 현황의 %는 총인구 대비 비중으로 Math.ceil()를 써서 소수점 이하를 제거해주었다.

 

<Open API 코로나 예방접종 현황>

// Open API 코로나 예방접종 현황
$(function () {
    // Open API request URL에 넣을 당일 날짜 생성하는 변수들
    const today = new Date();
    const year = today.getFullYear();
    const month = ('0'+ (today.getMonth() + 1)).slice(-2);
    const day = ('0' + today.getDate()).slice(-2);
    const dateString = year + '-' + month + '-' + day;

    $.ajax({
        async: true,
        url: 'https://api.odcloud.kr/api/15077756/v1/vaccine-stat' + '?page=1&perPage=1&cond%5BbaseDate%3A%3AGT%5D=' + dateString + '&cond%5BbaseDate%3A%3AGTE%5D=' + dateString + '&serviceKey=5efVUPw82kO8VF6ZqPGLMp9zqy%2BqakqBGhELrXviR4QlQ8c7Jq68hU3QRYYtfLkGl2PNXNT0OQcLrxRYwidOPg%3D%3D',
        data: {},
        method: 'GET',
        timeout: 3000,
        dataType: 'json',
        success: function(result) {
            console.log(result)
            let img1 = $('<img />')
            let img2 = $('<img />')
            let img3 = $('<img />')
            let imgUrl = "/static/main/img/main/vaccine_up_icon2.png"
            img1.attr('src', imgUrl, 'alt', '')
            img2.attr('src', imgUrl, 'alt', '')
            img3.attr('src', imgUrl, 'alt', '')
            $('.livedate').text("( " + dateString + " 기준, 2021-2-26 이후 누계, 단위: 명 )")

            // 소수점 자릿수 설정하는 함수 Ver. 3, toString() & replace()
            const n11 = result['data'][0]['totalFirstCnt'];
            const n12 = result['data'][0]['firstCnt'];
            const n21 = result['data'][0]['totalSecondCnt'];
            const n22 = result['data'][0]['secondCnt'];
            const n31 = result['data'][0]['totalThirdCnt'];
            const n32 = result['data'][0]['thirdCnt'];
            const cn11 = n11.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
            const cn12 = n12.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
            const cn21 = n21.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
            const cn22 = n22.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
            const cn31 = n31.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
            const cn32 = n32.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
            $('#percent1').text(Math.ceil(result['data'][0]['totalFirstCnt'] / 516255.61)+ "%")
            $('#person1T').text("누적 " + cn11 + "명")
            $('#person1N').text("신규 " + cn12 + "명").append(img1)
            $('#percent2').text(Math.ceil(result['data'][0]['totalSecondCnt'] / 516255.61)+ "%")
            $('#person2T').text("누적 " + cn21 + "명")
            $('#person2N').text("신규 " + cn22 + "명").append(img2)
            $('#percent3').text(Math.ceil(result['data'][0]['totalThirdCnt'] / 516255.61)+ "%")
            $('#person3T').text("누적 " + cn31 + "명")
            $('#person3N').text("신규 " + cn32 + "명").append(img3)
        },
        error: function() {
            alert('Open API(예방접종 현황)가 끌려오지 않습니다!')
        }
    });
})

 

<Open API 코로나 확진자 현황>

// Open API 코로나 확진자 현황
$(function () {
    // Open API request URL에 넣을 당일 날짜 생성하는 변수들
    const today = new Date();
    const year = today.getFullYear();
    const month = ('0' + (today.getMonth() + 1)).slice(-2);
    const tday = ('0' + today.getDate()).slice(-2);
    const yday = ('0' + (today.getDate() - 1)).slice(-2);
    const dateStringT = year + month + tday;
    const dateStringY = year + month + yday;

    $.ajax({
        async: true,
        url: 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson',
        data: {
            serviceKey: '5efVUPw82kO8VF6ZqPGLMp9zqy+qakqBGhELrXviR4QlQ8c7Jq68hU3QRYYtfLkGl2PNXNT0OQcLrxRYwidOPg==', // Decoding
            pageNo: '1',
            numOfRows: '1',
            startCreateDt: dateStringY,
            endCreateDt: dateStringT,
        },
        method: 'GET',
        timeout: 3000,
        dataType: 'XML',
        success: function(result) {
            console.log(dateStringY, dateStringT)
            console.log(result)
            $('#livedate2').text((today.getMonth() + 1) + "/" + today.getDate() + " 기준")

            let nums = $(result).find('decideCnt').text()
            const cNum1 = nums.substring(0, 7)
            const cNum2 = nums.substring(7, 15)
            const coronaNum = cNum1 - cNum2

            console.log(nums, cNum1, cNum2, coronaNum)

            $('#coronaNum').text(coronaNum.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",") + "명")
        },
        error: function() {
            alert('Open API(확진자 현황)가 끌려오지 않습니다!')
        }
    });
})

 

자가진단 페이지는 수업 때 배운 poll(투표 시스템)을 활용해서

models.py에 질문과 답변을 class로 만들어서 DB에 저장된 것들을 for문으로 모두 한 페이지에 보여주려 했지만..

 

한 페이지에 모든 문항을 끌고 오는 게 어려워서,
결국 조장인 창현님이 html에 질문과 답변을 모두 기재하고
radio 버튼에 id와 value를 부여하여 "예"만 집계할 수 있도록 만들어줬다. 😇 너무 잘해! 🤗

 

<자가진단 페이지_views.py>

from django.shortcuts import render, get_object_or_404, redirect
from diagnosis.models import Question, Choice
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.db.models import Q
from django.contrib import messages


def s_diagnosis(request):
    question = Question.objects.all().order_by('pub_date')
    choice = Choice.objects.filter(Q(choice_text='내용'))
    context = {
        'q_list': question,
        'c_list': choice
    }
    return render(request, 'diagnosis/main1.html', context)


def my_views(request):
    if request.method == 'POST':
        select_list = []
        for i in range(8):
            selected = request.POST.get('selected' + str(i))
            if selected is None:
                continue
            else:
                select_list.append(int(selected))

        result = sum(select_list)

        if len(select_list) != 8:
            messages.warning(request, '선택지를 모두 제출해 주세요.')
            return render(request, 'diagnosis/main2.html')

        else:
            if result < 4:
                messages.warning(request, '{} 표 입니다. 코로나가 의심되지 않지만 조심하세요. 5초 뒤 "코로나 증상 및 행동수칙" 페이지로 이동합니다.'.format(result))
                return render(request, 'diagnosis/main3.html')

            elif 4 <= result or result == 8:
                messages.warning(request, '{} 표 입니다. 코로나가 의심되오니 즉시 선별검사소나 병원을 방문하세요. 5초 뒤 "전문의에게 물어보세요" 페이지로 이동합니다.'.format(result))
                return render(request, 'diagnosis/main1.html')

 

<자가진단 페이지_HTML>

{% extends 'base.html' %}
{% load bootstrap4 %}

{% block html_header %}

<script>
    window.setTimeout(function() {
        $(".alert-auto-dismissible").fadeTo(500, 0).slideUp(500, function(){
            $(this).remove();

            setTimeout('go_probbs()', 1)

        });
    }, 5000);

    function go_probbs(){
        // window.location.href = {% url 'guide:guide' %}
        window.location.href = '{% url 'probbs:index' %}'
    }
</script>

<style>
h3, input::-webkit-input-placeholder, button {
  font-family: "roboto", sans-serif;
  transition: all 0.3s ease-in-out;
  margin-top: 20px;
}
{##bee0ff#}
h3 {
  height: 100px;
  width: 100%;
  font-weight: bold;
  font-size: 35px;
  background: #8557ab;
  color: white;
  line-height: 265%;
  text-align: center;
  border-radius: 3px 3px 0 0;
  box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.2);
}

form {
  box-sizing: border-box;
  width: 550px;
  margin: 30px auto 0;
  box-shadow: 2px 2px 5px 1px rgba(0, 0, 0, 0.2);
  padding-bottom: 40px;
  border-radius: 3px;
}
form h1 {
  box-sizing: border-box;
  padding: 20px;
}

input {
  margin: 40px 25px;
  width: 50px;
  display: block;
  border: none;
  padding: 10px 0;
  border-bottom: solid 1px #1abc9c;
  transition: all 0.3s cubic-bezier(0.64, 0.09, 0.08, 1);
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 96%, #1abc9c 4%);
  background-position: -200px 0;
  background-size: 200px 100%;
  background-repeat: no-repeat;
  color: #0e6252;
}
input:focus, input:valid {
  box-shadow: none;
  outline: none;
  background-position: 0 0;
}
input:focus::-webkit-input-placeholder, input:valid::-webkit-input-placeholder {
  color: #1abc9c;
  font-size: 11px;
  transform: translateY(-20px);
  visibility: visible !important;
}
{##1abc9c#}
button {
  border: none;
  background: #a889cb;
  cursor: pointer;
  border-radius: 3px;
  padding: 6px;
  width: 200px;
  color: white;
  margin-left: 180px;
  box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.2);
}
button:hover {
  transform: translateY(-3px);
  box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.2);
}

.follow {
  width: 42px;
  height: 42px;
  border-radius: 50px;
  background: #03A9F4;
  display: inline-block;
  margin: 50px calc(50% - 21px);
  white-space: nowrap;
  padding: 13px;
  box-sizing: border-box;
  color: white;
  transition: all 0.2s ease;
  font-family: Roboto, sans-serif;
  text-decoration: none;
  box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.2);
}
.follow i {
  margin-right: 20px;
  transition: margin-right 0.2s ease;
}
.follow:hover {
  width: 134px;
  transform: translateX(-50px);
}
.follow:hover i {
  margin-right: 10px;
}

strong {
    font-weight: bold;
    font-size: 18px;
}
</style>

{% endblock %}

{% block html_body %}

   {% for message in messages %}
        <div class="alert {{ message.tags }} alert-auto-dismissible alert-dismissible notification-container text-center" role="alert">
            <span aria-hidden="true"></span>
            {{ message }}
        </div>
   {% endfor %}

            <form action="{% url 'diagnosis:my_views' %}" method="post" class="requires-validation" novalidate>
<!--            <form action="/diagnosis/test/" class="requires-validation" method="post" novalidate>-->
                <h3>Self&nbsp;Diagnosis</h3>
                {% csrf_token %}
                <div class="col-md-12" style="margin-left: 30px;margin-top: 20px;">
                <strong>1. 체온이 37.5℃ 이상인가요? </strong>
                <br><br>
                <label for="selected0" >예</label>
                <input id="selected0" type="radio" name="selected0" value="1">
                <label for="selected0">아니오</label>
                <input id="selected0" type="radio" name="selected0" value="0">
                <br><br>
                <strong>2. 기침을 하나요?</strong>
                <br><br>
                <label for="selected1">예</label>
                <input id="selected1" type="radio" name="selected1" value="1">
                <label for="selected1">아니오</label>
                <input id="selected1" type="radio" name="selected1" value="0">
                <br><br>
                <strong>3. 숨 쉬기가 곤란한가요?</strong>
                <br><br>
                <label for="selected2">예</label>
                <input id="selected2" type="radio" name="selected2" value="1">
                <label for="selected2">아니오</label>
                <input id="selected2" type="radio" name="selected2" value="0">
                <br><br>
                <strong>4. 오한이 있나요?</strong>
                <br><br>
                <label for="selected3">예</label>
                <input id="selected3" type="radio" name="selected3" value="1">
                <label for="selected3">아니오</label>
                <input id="selected3" type="radio" name="selected3" value="0">
                <br><br>
                <strong>5. 근육통이 있나요?</strong>
                <br><br>
                <label for="selected4">예</label>
                <input id="selected4" type="radio" name="selected4" value="1">
                <label for="selected4">아니오</label>
                <input id="selected4" type="radio" name="selected4" value="0">
                <br><br>
                <strong>6. 두통이 있나요?</strong>
                <br><br>
                <label for="selected5">예</label>
                <input id="selected5" type="radio" name="selected5" value="1">
                <label for="selected5">아니오</label>
                <input id="selected5" type="radio" name="selected5" value="0">
                <br><br>
                <strong>7. 인후통이 있나요?</strong>
                <br><br>
                <label for="selected6">예</label>
                <input id="selected6" type="radio" name="selected6" value="1">
                <label for="selected6">아니오</label>
                <input id="selected6" type="radio" name="selected6" value="0">
                <br><br>
                <strong>8. 후각 또는 미각이 소실되었나요?</strong>
                <br><br>
                <label for="selected7">예</label>
                <input id="selected7" type="radio" name="selected7" value="1">
                <label for="selected7">아니오</label>
                <input id="selected7" type="radio" name="selected7" value="0">
                <br><br>
                </div>
                <button type="submit">확인하기</button>
                </div>
            </form>

{% endblock %}

 

여기서 관건은 세 가지 조건에 따라 다른 페이지로 자동 이동하게 해주는 것!


선택지가 모두 제출되지 않았을 경우 → 자가진단 페이지,
8개의 질문 중 예가 4개 미만일 경우 → 코로나 증상 및 행동수칙 페이지,
8개의 질문 중 예가 4개 이상일 경우 → 전문의에게 물어보세요 페이지로 넘어간다.

 

window.setTimeout으로 함수를 만들어 선택지가 submit 버튼을 눌러 POST 방식으로 들어오면,

선택의 len() 혹은 result(예의 합계)에 따라 안내 메시지(messages.warning)가 상단에 떴다가 사라지고,

자동으로 각각의 페이지로 이동한다.

 

여기서도 각각의 페이지로 이동하도록 조건문을 만드는데 좀 헤맸다. 😅

 

우리 프로젝트의 초초초대박인 기능은,

메인 화면의 나라별 코로나 누적 확진자 그래프(버튼을 누르면 마구마구 움직인다!),

병원 & 약국의 Map API(본인의 현재 위치가 잡히고 주변의 병원/약국 검색 가능!)

전문의에게 물어보세요(회원가입/로그인 한 사람들만 전문의가 상주하는 게시판에 문의할 수 있다!)!

 

기능 구현 끝나고 코드 리뷰 해줬는데 이해 안 가고요.. 뜯어봐야 하는데 마음만 한가득, 흑흑...

 

인터페이스 개발 프로젝트하느라 4조 고생 많았다요!!!!!

728x90