Custom Email 사용하기

  1. 다음 스마트 워크에 들어갑니다. 메인
  2. 개인을 항목을 신청합니다.
  3. 도메인 항목에 개인 도메인 주소를 넣으시면 되며 해당 블로그의 경우 hosting.kr을 이용하였습니다. 메인
  4. 양식에 맞게 항목을 넣고 다음 단계로 진입합니다.
  5. 다음 사진과 같이 소유권 확인을 위한 설정을 진행을 하게 됩니다. 메인
  6. MX서버주소를 맞게 넣어주시면 됩니다. 메인
  7. 적용이 끝나면 스마트 워크 서비스 관리탭에서 지금 확인버튼을 눌러 바로 적용을 시킬 수 있습니다. (호스팅 업체의 사정에 따라 시간이 오래 걸릴 수 있습니다.)
  8. 확인이 끝나면 바로 사용할 수 있습니다.

SES 사용의 이유

gmail또는 naver같은 기존에 있는 웹서비스가 아닌 개인 또는 기업용 이메일(domain이 일반 웹서비스와 다른)을 사용하게 되면 외부로 메일을 보낼때 검증이 되지 않아 일부 메일에서 스팸메일로 빠지는등의 예상치 못한 오류가 발생을 합니다.
아마존 SES는 해당 오류를 DKIM 인증을 통해 진행을 해주며 매우 저렴한 금액으로 사용할 수 있습니다. 또한 서버 구축 또는 비동기 처리를 하지 않고 AWS에 request만 보내면 처리를 할 수 있어 많이 알려지지는 않았지만 참 좋은 서비스라고 생각됩니다.

ses

Simple Email Service

SES

리전은 크게 상관은 없으나 오레곤이 한국에서 가장 가까운 곳으로 판단됩니다.

도메인 검증

  1. 좌측 대시 보드의 domains에 들어갑니다.
  2. Verify a New Domain을 클릭합니다. 도메인 검증
  3. 본인의 Domain을 넣고 Generate DKIM Settings에 체크 후 Verify This Domain을 클릭합니다. 도메인 검증2
  4. 나오는 설정 값을 각자의 호스팅 업체의 도메인 설정에 들어가 설정합니다. 도메인 검증3
  5. 일정 시간이 흐르면 자동으로 도메인 검증이 완료 됩니다.
  6. 검증이 끝나고 나면 다음과 같은 메일을 받을 수 있습니다. 도메인 검증 완료
  7. DKIM 인증 또한 자동으로 메일이 발송됩니다. DKIM 검증 완료

DKIM이란 Domain Keys Indentified Mail의 약자로써 해당 주소에서 왔다고 하는 메일이 실제로 도메인 소유자에 의해 승인이 되었는지 확인할 수 있는 Email인증 방법이며 SES에서는 SES에서 해당 인증 또한 같이 진행을 받을 수 있습니다.

이메일 검증

  1. 좌측 대시 보드의 Email Addresses에 들어갑니다.
  2. Verify a New Email Address를 클릭합니다.
  3. 가지고 있는 해당 도메인에 연결된 메일을 입력하고 Verify This Email Address를클릭합니다.
  4. 인증용 이메일이 방송 되며 Status가 Pending Verification으로 되어 있음을 확인 할 수 있으며 메일이 발송되지 않았을 경우 Resend를 눌러 재발송을 요청할 수 있습니다. 이메일 검증
  5. 해당 이메일에 들어가게 되면 아마존에서 발송한 검증용 이메일이 확인이 가능합니다.
  6. 해당 메일의 검증용 URL에 들어가게 되면 이메일 검증 후 다음과 같은 화면을 볼 수 있습니다. 이메일 검증 완료
  7. 다시 SES화면에 들어가게 되면 메일이 검증 되었음을 확인할 수 있습니다. 이메일 검증 완료2
  8. 해당 이메일 클릭후 Sending Test Email을 클릭하게 되면 다음과 같은 창이 나오며 테스트용 이메일을 발송할 수 있습니다. 이메일 보내기
  9. 메일 원문을 확인하면 발송자는 ses이며 DKIM서명 또한 적용이 되어 있음을 알 수 있습니다.

Django에서의 설정

굳이 따로 패키지를 설치하지 않아도 상관이 없으며 django의 기본 이메일을 사용하듯이 사용화면 되며 settings의 설정 작업만 조금 필요합니다.

# settings.py

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'email-smtp.us-west-2.amazonaws.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = config_secret_common['aws']['ses']['key_id']
EMAIL_HOST_PASSWORD = config_secret_common['aws']['ses']['secret_key']
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'mail@vouloir.net'
SERVER_EMAIL = 'mail@vouloir.net'

HTML

<td>
    {% if ids %}
        {% for m in ids %}
            <a href="/merchant/{{m.id}}/" target=_blank>{{m.name}}</a>
        {% endfor %}
    {% else %}
        연결된 가맹점이 없습니다.<br>
        {% if w|slice:":2" != '01' %}
            <button class="btn btn-info btn-search" data-phone="{{ w }}">웹에서 검색하기</button>
        {% endif %}
    {% endif %}
</td>

<script>
$(document).ready(function () {
    $('.btn-search').on('click', function () {
        if($(this).attr('aria-describedby') === undefined){
            crwaling_merchant_info($(this).data('phone'), $(this));
        }else{
            var popover_id = $(this).attr('aria-describedby');
            $('#'+popover_id).remove();
            $(this).removeAttr('data-original-title title aria-describedby');
        }
    })
});
</script>

핸드폰번호로 웹에서 검색을 할경우 상호명등이 나오지 않으므로 templatefilter slice를 이용해 01로 시작하는 번호는 검색기능을 넣지 않음

JS

function crwaling_merchant_info(phone, ele) {
    $.ajax({
        url: '/ajax/crwaling_merchant_info/',
        type: 'post',
        dataType: 'json',
        data: {
            phone: phone
        },
        cache: false,
        async: true,
        success: function (json) {
            if (!json['success']){
                alert('처리 중 오류가 발생하였습니다.');
                $(ele).removeAttr('disabled');
                return false;
            }
            $(ele).popover({
                title: json['merchant']['name'],
                content: json['merchant']['address']
            }).popover('show').removeAttr('disabled');
            return true;
        },
        error: function (err) {
            $(ele).popover({
                content: '해당 번호에 대한 정보를 찾을 수 없습니다.'
            }).popover('show').removeAttr('disabled');
        },
        beforeSend: function () {
            $(ele).attr('disabled', true);
        }
    })
}

오류 발생시 alert를 발생하며 button의 disabled attr 제거

성공시 popover를 통해 가게 이름과 주소를 보여줌

crawling 메서드

def phone_crawler(phone):
    naver_params = {
        'sm': 'mtb_hty.top',
        'where': 'm',
        'query': phone,
    }
    headers = {'Referrer': 'always'}
    naver_search_url = 'https://m.search.naver.com/search.naver?' + urllib.urlencode(naver_params)
    naver_response = requests.get(naver_search_url, headers=headers)
    naver_soup = BeautifulSoup(naver_response.text, "html.parser")
    print(naver_response)
    naver_parse_data = naver_soup.select_one('.item_info')
    if naver_parse_data:
        merchant = {
            "name": naver_parse_data.select_one('span.tit').get_text().encode('utf-8'),
            "address": naver_parse_data.select_one('span.address').get_text().encode('utf-8'),
        }
        return merchant
    daum_params = {
        'w': 'tot',
        'nil_mtopsearch': 'btn',
        'DA': 'YZR',
        'q': phone,
    }

    daum_url = 'https://m.search.daum.net/search'
    daum_search_url = daum_url + "?" + urllib.urlencode(daum_params)
    daum_response = requests.get(daum_search_url)
    daum_soup = BeautifulSoup(daum_response.text, "html.parser")

    daum_parse_data = daum_soup.select_one('div.coll_cont')
    if daum_parse_data:
        merchant = {
            "name": daum_parse_data.select_one('.txt_name').get_text().encode('utf-8'),
            "address": daum_parse_data.select_one('#poiAddressLabel').get_text().encode('utf-8'),
        }
        return merchant

    return None


네이버 검색시 header안에 refferer를 넣어야 함 안넣을 경우 403에러 발생

네이버에 요청을 하여 data를 파싱하여 값을 넣고 리턴을 하며 값이 없을 경우 daum쪽에 검색을 한다.

리턴된 값은 crwaling_merchant_info의 merchant값으로 전달되어 popover로 나타난다.

결과

info_crawaling_from_number

추가

해당 데이터를 client단에 cache화 하여 과하게 많은 요청을 보내지 않도록 수정 필요

패키지 설치

공식 document
참고 사이트

$ pip install weasyprint
$ brew install cairo
$ brew install pango

weasyprint의 경우 그래픽 라이브러리인 cairo와 font관련 라이브러리인 Pango에 의존성을 갖고 있습니다.
또한 pango의 경우 로컬에 있는 font를 사용하기때문에 PC혹은 서버에 한글 font를 설치해야합니다. (한글 폰트 및 의존성 있는 패키지때문에 많은 고생을 했습니다.)

코드 작성

# views.py

def html_to_pdf_view(request, pk):
    advice = get_object_or_404(Advice, pk=pk)
    chat_list = advice.chat_set.all()
    html_string = render_to_string('utils/advice_pdf.html', {'chat_list': chat_list})

    html = HTML(string=html_string)
    html.write_pdf(target='/tmp/mypdf.pdf')

    fs = FileSystemStorage('/tmp')
    with fs.open('mypdf.pdf') as pdf:
        response = HttpResponse(pdf, content_type='application/pdf')
        response['Content-Disposition'] = 'attachment; filename="mypdf.pdf"'
        return response

# utils/advice_pdf.html

<!doctype html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1 style="text-align: center">상담창</h1>
    <ul style="list-style-type: none;">
    {% for chat in chat_list %}
        <li>
        {% if chat.user.is_superuser %}
            <div>
                <div style="position: relative; display: inline-block; width: 48px; height: 48px; margin-top: 7px; border-radius: 25px;">
                    <span></span>
                </div>
                <div style="display: inline-block; margin-top: 7px; padding: 12px 15px; float: left; border-radius: 0px 12px 12px; background-color: #f0f0f0; color: #333; font-size: 12px; line-height: 17px;">
                    {{ chat.content }}
                </div>
                <div style="display: block; padding-left: 12px; text-align: left; color: #cfcfcf; font-size: 9px; line-height: 19px;">
                    {{ chat.created_at }}
                </div>
            </div>
        {% else %}
            <div>
                <div style="position: relative; display: inline-block; width: 48px; height: 48px; margin-top: 7px; border-radius: 25px;">
                    <span></span>
                </div>
                <div style="display: inline-block; margin-top: 7px; padding: 12px 15px; float: right; border-radius: 0px 12px 12px; border-top-left-radius: 12px; border-top-right-radius: 0px; font-size: 12px; line-height: 17px;  background-color: #4fd2c2; color: white;">
                    {{ chat.content }}
                </div>
                <div style="display: block; padding-right: 15px; text-align: right; color: #cfcfcf; font-size: 9px; line-height: 19px;">
                    {{ chat.created_at }}
                </div>
            </div>
        {% endif %}
        </li>
    {% endfor %}
    </ul>
</body>
</html>

결과물

html_to_pdf

정리

템플릿 태그인 if for등은 사용이 가능하나 request.user같은 부분은 사용이 불가능함
css파일을 이용할 수 있는 것 같으나 정확하게 알 수 없어 html의 모든 태그들 안에 css를 작성함 추후 css파일 작성해서 진행해볼 예정
대부분의 기능이 작동을 해서 매우 편하게 쓸 수 있는 패키지인 것 같다ㅜ소

지난 블로그에서 custom template tag를 이용해 데이터를 로드 하는 방법에 대한 글을 적었다. 금일 스터디에서 조금 더 편하고 정석적인 방법을 알게되어 소개하고자 한다.

template context-processors

context-processors 공식문서 간단히 정리하자면 HttpRequest를 인수로 사용하고 사전형(dict) 반환해준다는 내용이다.
설명보다는 코드를 보는게 더 이해가 빠르다.

함수 생성

  1. 루트디렉토리(manage.py와 같은 위치)에 python package를 하나 생성한다.
  2. 해당 블로그에서는 utils라는 이름으로 생성하였다.
  3. 해당 패키지 아래에 context_processors라는 파이썬 파일을 하나 생성한다.(이름을 꼭 맞출 필요는 없는 것 같다)
  4. 간단하게 위에서 말한 내용처럼 HttpRequest를 인수로 받으며 dict형을 반환하는 간단한 함수를 제작한다.
# utils/context_processors.py

def categories(request):
    return {'blog_categories': BlogMainCategory.objects.all(), }

settings.py 설정

생성한 함수를 어디서든 불러올 수 있어야하므로 settings.py의 template(template변수로 사용할 것이므로)의 options 아래의 context_processors에 넣어준다.

# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        ...
        'OPTIONS': {
            'context_processors': [
            		...
            		'utils.context_processors.categories',
            ],
        },
    },
]

사용

이제 준비는 다 끝났다 그대로 가져와 사용만 하면 된다. 지난번에 하던것처럼 {% load custom_tags %}같은 내용도 필요 없다 바로 템플릿에서 사용할 수 있다.

<ul class="submenu">
    {% for blog_category in blog_categories %}
        <li class="has-submenu">
            <a>{{ blog_category }}</a>
            <ul class="submenu">
                {% for subcategory in blog_category.blogsubcategory_set.all %}
                    <li><a href="{{ subcategory.get_absolute_url }}">{{ subcategory.name }}</a></li>
                {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

날로 먹는 개발 블로그 끝! ㅏㄱ