본문 바로가기
Python/Django(장고)

[Python/Django] 파이썬 주식 정보(뉴스) 페이지 만들기 - 1. 구조 만들기

by 깐테 2021. 7. 6.

2021.07.05 - [Python/Django(장고)] - [Python/Django] 파이썬 장고로 웹페이지 만들기 - 4. DB 연결

 

[Python/Django] 파이썬 장고로 웹페이지 만들기 - 4. DB 연결

2021.07.02 - [Python/Django(장고)] - [Python/Django] 파이썬 장고로 웹페이지 만들기 - 3. 모델, 어드민, 템플릿 태그 [Python/Django] 파이썬 장고로 웹페이지 만들기 - 3. 모델, 어드민, 템플릿 태그 2021.07..

kante-kante.tistory.com

지난 포스트들의 내용을 사용하기 때문에 이전 포스트를 참고하시면 좋습니다.

 

 

지난 포스트에서는 로컬 PC에 데이터들을 넣고, 템플릿 태그를 이용하여 html에 표시해보았다.

 

이번에는 한국거래소, 네아버 파이낸스에서 주식정보를 받아와서 로컬PC의 DB에 삽입해보는 테스트를 진행하도록 하겠다.

 


 

1. DB 가져오기


 

https://github.com/INVESTAR/StockAnalysisInPython/blob/master/05_Stock_Price_API/Investar/DBUpdaterEx.py

 

INVESTAR/StockAnalysisInPython

Contribute to INVESTAR/StockAnalysisInPython development by creating an account on GitHub.

github.com

 

DB를 업데이트 하는 코드는 해당 깃허브를 참조하였다.

 

한국거래소와 네이버 금융 홈페이지에서 DB정보를 받아온 다음, 이것을 DB에 삽입하는것까지 지원하는 코드이다.

 

#DBUpdater.py

import pandas as pd
from bs4 import BeautifulSoup
import requests
import urllib, pymysql, calendar, time, json
from urllib.request import urlopen
from datetime import datetime
from threading import Timer
import lxml

# 한국거래소 상장종목 DB 업데이트
class DBUpdater:
    def __init__(self):
        ''' 유의사항:
            해당 코드는 date가 primary key로 설정되지 않으면 date값을 여러개를 가져오지 못한다.  
            DB에서 직접 date를 기본키로 설정 해주는 것이 편하다.
        '''
        self.conn=pymysql.connect(host='localhost', user='root', password='비밀번호',db='stock_test',charset='utf8')
        
        with self.conn.cursor() as curs:
            sql="""
            CREATE TABLE IF NOT EXISTS stock_company(
                code VARCHAR(20),
                company VARCHAR(40),
                last_update DATE,
                PRIMARY KEY(CODE)
            );
            """
            curs.execute(sql)
            sql="""
            CREATE TABLE IF NOT EXISTS stock_daily(
                code VARCHAR(20),
                company VARCHAR(40),
                date DATE,
                open BIGINT(20),
                high BIGINT(20),
                low BIGINT(20),
                close BIGINT(20),
                diff BIGINT(20),
                volume BIGINT(20),
                PRIMARY KEY(CODE,DATE)
            );
            """
            curs.execute(sql)
        self.conn.commit()

        self.codes = dict()
    
    def __del__(self):
        '''소멸자 정의'''
        self.conn.close()
    
    def read_krx_code(self):
        '''KRX로부터 상장법인목록 파일을 읽어 데이터프레임으로 변환'''
        url = 'https://kind.krx.co.kr/corpgeneral/corpList.do?method='\
            'download&searchType=13'
        krx = pd.read_html(url, header=0)[0]
        krx = krx[['종목코드','회사명']]
        krx = krx.rename(columns={'종목코드':'code', '회사명':'company'})
        krx.code = krx.code.map('{:06d}'.format)
        return krx

    def update_comp_info(self):
        '''주식 시세를 stock_company 테이블에 업데이트한 후 딕셔너리 저장'''
        sql="SELECT * FROM stock_company"
        df = pd.read_sql(sql, self.conn)
        for idx in range(len(df)):
            self.codes[df['code'].values[idx]]=df['company'].values[idx]
        
        with self.conn.cursor() as curs:
            sql = "SELECT max(last_update) FROM stock_company"
            curs.execute(sql)
            rs = curs.fetchone()
            today = datetime.today().strftime('%Y-%m-%d')

            if rs[0] == None or rs[0].strftime('%Y-%m-%d')<today:
                krx=self.read_krx_code()
                for idx in range(len(krx)):
                    code = krx.code.values[idx]
                    company = krx.company.values[idx]
                    sql = f"REPLACE INTO stock_company (code, company, last"\
                        f"_update) VALUES ('{code}','{company}','{today}')"
                    curs.execute(sql)
                    self.codes[code] = company
                    tmnow = datetime.now().strftime('%Y-%m-%d %H:%M')
                    print(f"[{tmnow}] {idx: 04d} REPLACE INTO stock_company "\
                        f"VALUES ({code},{company},{today})")
                self.conn.commit()
                print('')

    def read_naver(self, code, company, pages_to_fetch):
        '''네이버 금융에서 주식 시세를 읽어 데이터프레임으로 변환'''
        try:
            url = f"http://finance.naver.com/item/sise_day.nhn?code={code}"
            html = BeautifulSoup(requests.get(url,
                headers={'User-agent': 'Mozilla/5.0'}).text, "lxml")
            pgrr = html.find("td", class_="pgRR")
            if pgrr is None:
                return None
            s = str(pgrr.a["href"]).split('=')
            lastpage = s[-1] 
            df = pd.DataFrame()
            pages = min(int(lastpage), pages_to_fetch)
            for page in range(1, pages + 1):
                pg_url = '{}&page={}'.format(url, page)
                df = df.append(pd.read_html(requests.get(pg_url,
                    headers={'User-agent': 'Mozilla/5.0'}).text)[0])                                          
                tmnow = datetime.now().strftime('%Y-%m-%d %H:%M')
                print('[{}] {} ({}) : {:04d}/{:04d} pages are downloading...'.
                    format(tmnow, company, code, page, pages), end="\r")
            df = df.rename(columns={'날짜':'date','종가':'close','전일비':'diff'
                ,'시가':'open','고가':'high','저가':'low','거래량':'volume'})
            df['date'] = df['date'].replace('.', '-')
            df = df.dropna()
            df[['close', 'diff', 'open', 'high', 'low', 'volume']] = df[['close',
                'diff', 'open', 'high', 'low', 'volume']].astype(int)
            df = df[['date', 'open', 'high', 'low', 'close', 'diff', 'volume']]
        except Exception as e:
            print('Exception occured :', str(e))
            return None
        return df

    def replace_into_db(self, df, num, code, company):
        '''네이버 금융에서 읽어온 주식 시세를 DB에 replace'''
        with self.conn.cursor() as curs:
            for r in df.itertuples():
                sql = f"REPLACE INTO stock_daily VALUES ('{code}', '{company}',"\
                    f"'{r.date}', {r.open}, {r.high}, {r.low}, {r.close}, "\
                    f"{r.diff}, {r.volume})"
                curs.execute(sql)
            self.conn.commit()
            print('[{}] #{:04d} {} ({}) : {} rows > REPLACE INTO stock'\
                '_daily [OK]'.format(datetime.now().strftime('%Y-%m-%d'\
                ' %H:%M'), num+1, company, code, len(df)))

    def update_daily_price(self, pages_to_fetch):
        '''KRX 상장법인 주식시세를 네이버로부터 읽어 DB에 업데이트'''
        for idx, code in enumerate(self.codes):
            df = self.read_naver(code, self.codes[code], pages_to_fetch)
            if df is None:
                continue
            self.replace_into_db(df, idx, code, self.codes[code])

    def execute_daily(self):
        '''실행 즉시 daily_price 테이블 업데이트'''
        self.update_comp_info()
        
        try:
            with open('config.json', 'r') as in_file:
                config = json.load(in_file)
                pages_to_fetch = config['pages_to_fetch']
        except FileNotFoundError:
            with open('config.json', 'w') as out_file:
                pages_to_fetch = 2
                config = {'pages_to_fetch': 1}
                json.dump(config, out_file)
        self.update_daily_price(pages_to_fetch)


if __name__=='__main__':
    dbu=DBUpdater()
    dbu.execute_daily()

 

 

코드를 약간 수정해주었다.

 

이 DBUpdater를 실행하기 전에 설치해야할 모듈이 있는데,  pandas와 BeautifulSoup 모듈이다. + requests

 

pip install pandas
pip install bs4
pip install requests

 

 

1. Pandas

 

Pandas는 데이터분석 라이브러리로, 행과 열로 이루어진 데이터 객체를 만들어 다룰 수 있게 되며 보다 안정적으로 대용량의 데이터들을 처리하는데 매우 편리한 도구 입니다.

출처: https://doorbw.tistory.com/172 [Tigercow.Door]

 

 

2. BeautifulSoup

 

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#beautifulsoup

 

Beautiful Soup Documentation — Beautiful Soup 4.9.0 documentation

Non-pretty printing If you just want a string, with no fancy formatting, you can call str() on a BeautifulSoup object (unicode() in Python 2), or on a Tag within it: str(soup) # ' I linked to example.com ' str(soup.a) # ' I linked to example.com ' The str(

www.crummy.com

BeautifulSoup의 경우에는 크롤링을 용이하게 도와주는 모듈이며. 공식 문서는 위 사이트이긴 하나... 전체가 영어 원문인데다가 양이 많다.

 

그냥 참고정도만 하는것이 좋겠다.

 

 

3. requests

 

https://kk-7790.tistory.com/45

 

Requests 모듈 사용법

Python을 이용해 크롤링 소스를 짤때 많이 사용하는 모듈이다. Requests 와 BeautofulSoup을 사용하는데, 이번 페이지에선 Requests 사용법에 대해 알아보자. Requests 모듈 이란?  - 웹페이지에서 HTTP 요청을.

kk-7790.tistory.com

 - 웹페이지에서 HTTP 요청을 보내 원하는 HTML 정보를 가져오는 모듈이다.

 - requests 모듈을 사용하기 위해서는 웹페이지에서 요청하는 방식인 post와 get 방식을 알아야할 필요가 있다.

   그 이유는 어떤 방식이냐에 따라 parameter(매개변수)를 전달하는 방법이 다르기 때문이다.

 - post인지 get 방식인지 확인하는 방법은 크롬기준으로 개발자 도구(Ctrl + shift + i) -> Network 목록에 들어가면 수많은 소스가 나오는데 아무 페이지나 이동후 소스파일을 확인해보면 Request Method : 에서 어떤 방식인지 확인할 수 있다.

 

해당 블로그의 내용을 참조하였다.

 

https://pypi.org/project/requests/

 

requests

Python HTTP for Humans.

pypi.org

 

pypi에도 requests에 관한 내용이 짤막하게 정리되어 있는데, 간단히 말해서 HTTP 라이브러리라고 생각하면 된다.

 


2. DBUpdater 실행하기


우측 상단에 재생버튼을 눌러 Updater를 실행시키면

 

오늘의 날짜로 잘 들어가는 모습을 확인할 수 있다.

 

DB 정보를 잘 가져오는 모습을 확인할 수 있다.

 

 #DBUpdater.py
 
 def execute_daily(self):
        '''실행 즉시 daily_price 테이블 업데이트'''
        self.update_comp_info()
        
        try:
            with open('config.json', 'r') as in_file:
                config = json.load(in_file)
                pages_to_fetch = config['pages_to_fetch']
        except FileNotFoundError:
            with open('config.json', 'w') as out_file:
                pages_to_fetch = 2
                config = {'pages_to_fetch': 1}
                json.dump(config, out_file)
        self.update_daily_price(pages_to_fetch)

 

위에서 업데이트되고 있는 DB를 보면, 20줄씩. 그러니까 20일간의 데이터들을 불러오고 있는 모습을 확인할 수 있다.

 

해당 코드에서 pages_to_fetch = 2 라고 된 부분을 수정하면 오래된 주식 정보까지도 가지고 올 수 있다.

 

1= 10개씩이라고 생각하면 되며 왜 이렇게 가져오게 되는지 살펴보면

 

 

삼성전자의 일별 시세 정보. 네이버 금융 사이트에 따로 정리되어 있다.

 

1페이지를 살펴보면 6월 23일부터 오늘 날짜까지 10개의 정보가 정리되어 있는 모습을 볼 수 있다.

 

1페이지에 10개씩 정리되어 있으므로, pages_to_fetch = 2 라고 된 부분이 왜 20개의 데이터를 가져오는지 알 수 있다.

 

결국 적은, 혹은 최신 정보를 가져오려면 페이지를 줄이면 되고, 오래된 정보를 가져오고 싶으면 페이지 수를 늘려서 정보를 가져오면 된다.

 

 

DBUpdater.py가 있는 폴더에 config.json이 생성된 모습.

 

DB 업데이트를 완료하고 나면 config.json 파일이 생성된 것을 확인할 수 있다.

이는 DB정보를 한번 업데이트 하고 나면 굳이 오래된 정보까지 다시 가지고 올 필요가 없기 때문에

한번 가지고 온 다음부터는 1페이지의 정보만 가지고 오라고 설정한 것이다.

 

만약 다시 더 많은 정보를 가져오려고 한다면 해당 파일에 있는 pages_to_fetch 파일의 숫자를 수정해주거나 파일을 지우고 코드에서 숫자를 수정해줘도 된다.

 

 

그리고 heidisql을 이용하여 데이터를 확인해보면 값이 정상적으로 잘 들어간 것을 확인할 수 있다.

 

 


3. django에서 수정하기

1. views.py 수정하기

# views.py

from django.shortcuts import render
#edit
from .models import Company
from django.core.paginator import Paginator
from django.db.models import Q

def index(request):
    #edit
    company_list = Company.objects.all()
    context = {'company_list': company_list}

    return render(request, 'stock/index.html', context)

def company(request):
    comp = Company.objects.all()
    page = request.GET.get('page', '1')  # 페이지
    kw = request.GET.get('kw','') # 검색어
    #조회
    company_list = comp.order_by('code')
    if kw:
        company_list = company_list.filter(
            Q(code__icontains=kw) |
            Q(company__icontains=kw)
        ).distinct()

    #페이징처리
    paginator = Paginator(company_list, 20)
    page_obj = paginator.get_page(page)
    context = {'comp':page_obj, 'page':page, 'kw' : kw}
    return render(request, 'stock/company.html', context)

views.py를 다음과 같이 처리해준다.

 

페이징 처리를 위한 모듈을 import 해주었으며, 다른 페이지에 데이터를 추가해주기 위해 urls도 바꿔줄 것이다.

 

한 페이지에 20개의 데이터를 보여주도록 처리하였다.

 

https://wikidocs.net/71240

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

페이징 처리는 위 페이지를 참고하면 된다.

 

 

2. urls 수정

 

#[앱이름]/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('home/', views.index, name='home'),
    path('company/', views.company, name = 'company')
]

먼저 앱단의 urls를 다음과 같이 수정해 주었다.

 

views에 정의된 각 함수들을 연결시켜준다.

 

 

#config > urls.py

from django.contrib import admin
from django.urls import path,include
#edit
from stock import views
from stock import views as company

urlpatterns = [
    path('admin/', admin.site.urls),
    #---edit---
    path('',include('stock.urls')),
    path('company/',company.company),

]

그리고 config에 있는 urls.py도 다음과 같이 바꿔준다.

구분짓기 위해 company.company라고 해준 것이지 views.company라고 해도 상관 없다.

 

 

3. 템플릿 추가

 

<!-- templates\앱이름\company.html -->

{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<!DOCTYPE html>
<html lang="ko">
<head>
  <title>코스피 기업정보</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="">
  <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
  <meta name="generator" content="Hugo 0.83.1">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> 
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>

  <link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/dashboard/">
  
    <!-- Bootstrap core CSS -->
    <link href="/docs/5.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">

    <!-- 해당 부분이 빠지면 페이징 동작 안함-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
    <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
  
    <!-- Favicon -->
    <meta name="theme-color" content="#7952b3">

    <style>
      .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        user-select: none;
      }

      @media (min-width: 768px) {
        .bd-placeholder-img-lg {
          font-size: 3.5rem;
        }
      }
    </style>

</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container-fluid">
    <a class="navbar-brand" href="/home">주식정보사이트</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor02" aria-controls="navbarColor02" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarColor02">
      <ul class="navbar-nav me-auto">
        <li class="nav-item">
          <a class="nav-link" href="/dashboard">대시보드
            <span class="visually-hidden"></span>
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/daily_price">일일 가격 정보</a>
        </li>
        <li class="nav-item">
          <a class="nav-link active" href="/company">상장 기업 목록</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/search">뉴스</a>
        </li>
      </ul>
    </div>
  </div>
</nav>

<div class="container my-5">
<!-- ------------------------------- [edit] -------------------------------- -->
    <div class="row justify-content-end my-3">
        <div class="col-4 input-group">
            <input type="text" class="form-control kw" value="{{ kw|default_if_none:'' }}">
            <div class="input-group-append">
                <button class="btn btn-outline-secondary" type="button" id="btn_search">찾기</button>
            </div>
        </div>
    </div>
<!-- ----------------------------------------------------------------------- -->
    <table class="table table-striped" style="font-size:15px;">
        <thead>
        <tr class="thead-dark">
            <td><B>티커명</B></td>
            <td><B>기업명</B></td>
            <td><B>업데이트 날짜</B></td>
        </tr>
        </thead>
        <tbody>
        {% for company in comp %}
        <tr>
            <td>{{company.code}}</td>
            <td>{{company.company}}</td>
            <td>{{company.last_update}}</td>
        </tr>
        {% endfor %}
        <tbody>
    </table>
    <!-- 페이징처리 시작 -->
    <ul class="pagination justify-content-center">
        <!-- 이전페이지 -->
        {% if comp.has_previous %}
        <li class="page-item">
        <!-- edit -->
            <a class="page-link" data-page="{{ comp.previous_page_number }}" href="#">이전</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">이전</a>
        </li>
        {% endif %}
        <!-- 페이지리스트 -->
        {% for page_number in comp.paginator.page_range %}
        {% if page_number >= comp.number|add:-5 and page_number <= comp.number|add:5  %}
            {% if page_number == comp.number %}
            <li class="page-item active" aria-current="page">
            <!-- edit -->
                <a class="page-link" data-page="{{ page_number }}" herf="#">{{ page_number }}</a>
            </li>
            {% else %}
            <li class="page-item">
            <!-- edit -->
                <a class="page-link" data-page="{{ page_number }}" href="#">{{ page_number }}</a>
            </li>
            {% endif %}
        {% endif %}
        {% endfor %}
        <!-- 다음페이지 -->
        {% if comp.has_next %}
        <li class="page-item">
        <!-- edit -->
            <a class="page-link" data-page="{{ comp.next_page_number }}" href="#">다음</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
        </li>
        {% endif %}
    </ul>
    <!-- 페이징처리 끝 -->

<!-- edit -->
<form id="searchForm" method="get" action="{% url 'company' %}">
    <input type="hidden" id="kw" name="kw" value="{{ kw|default_if_none:'' }}">
    <input type="hidden" id="page" name="page" value="{{ page }}">
</form>
<!-- -->
{% block script %}
<script type='text/javascript'>
$(document).ready(function(){
    $(".page-link").on('click', function() {
        $("#page").val($(this).data("page"));
        $("#searchForm").submit();
    });

    $("#btn_search").on('click', function() {
        $("#kw").val($(".kw").val());
        $("#page").val(1);  // 검색버튼을 클릭할 경우 1페이지부터 조회한다.
        $("#searchForm").submit();
    });
});
</script>
{% endblock %}

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>

<!-- jQuery JS -->
<script src="{% static 'jquery-3.6.0.min.js' %}"></script>
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap.min.js' %}"></script>

<script src="/docs/5.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script src="dashboard.js"></script>
</body>

 

 

templates > [앱이름] 폴더에 company.html이라는 이름으로 파일을 다음과 같이 추가해준다.

 

너무 난잡하게 쿼리를 불러오고 스크립트를 불러와서 정신없긴 하지만, 수정은 추후에 다시 진행하도록 하고

다음과 같이 만들어주도록 한다.

 

최상단에 {% load static %}을 한 이유는 추후에 포스팅하겠지만, 부트스트랩을 이용하여 꾸밀 것이기 때문에 미리 import해주었다.

 

 

https://wikidocs.net/71806

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

 

검색 및 정렬 기능이나 페이징 처리는 해당 페이지를 참고하여 만들었다.

 

 

 

4. 서버 실행하기

 

python manage.py runserver

테이블 형태로 잘 표시되는 모습

 

그러면 이렇게 테이블 형태로 간단하게 잘 표시되는 모습을 확인할 수 있다.

 

 

하단부 페이지를 클릭하기 전.

 

하단에도 페이지를 클릭하기 전에는 1페이가 표시되도록 처리되어 있는데

 

 

페이지 처리가 잘 되는 모습

 

페이지 처리가 잘 되는 모습을 확인할 수 있다.

 

검색도 잘 되는 모습

검색도 잘 되는 모습을 확인할 수 있다.

 

 

다음 포스트에서는 간단한 크롤링을 이용하여 웹에서 정보를 표시하도록 해보겠다.

반응형