파이썬으로 방문자 등록 웹페이지 구현하기

파이썬으로 방문자 등록 웹페이지 구현하기 시작

파이썬으로 방문자 등록 웹페이지 구현하기 추진을 왜 했을까?

회사에서 돈을 안 들이고 방문자 등록 시스템을 만들고 싶다고 한다…

어디 다른 데서 챗GPT로 코드 작성해서 했다고..

거긴 IT팀에 개발자도 있는 기관인데…^^

각설하고 챗GPT와 파이썬을 통해 방문자 등록 시스템을 구현해보고자 합니다.

우선 사용할 코어 라이브러리는 Flask(Welcome to Flask — Flask Documentation (3.0.x) (palletsprojects.com))입니다.

파이썬으로 웹서비스를 구현하는 데 있어서 가장 많이 사용되는 라이브러리로 알고 있습니다.

준비사항

이번 프로젝트에 사용한 준비물은 다음과 같습니다:

과정

Flask 설치 및 기본 구조 세팅

Flask 설치하기

#python IDE의 터미널에서 flask 설치 또는 패키지에서 인스톨
pip install flask

기본 구조 세팅

기본폴더/
├── app.py # 기본 뼈대
├── templates/
│   ├── index.html # 방문자 등록 등 기본 페이지
│   ├── thank_you.html # 방문자 등록 완료시 나타날 페이지
│   ├── retrieve.html # 방문자 확인 페이지
│   └── mgmt_web.html # 관리 페이지
│   └── login.html # 관리 페이지 로그인
│   └── edit_visiotr.html # 관리 페이지 방문자 편집
│   └── logs.html # 관리 페이지에서 로그 확인
├── static/
│   └── style.css
└── visitors.db

방문자 등록 애플리케이션 구현

파이썬으로 방문자 등록 웹페이지 구현하기의 코어인 app.py를 작성합니다. 여기서는 라우팅, 데이터베이스 설정 및 폼 처리 등을 담당합니다.

from flask import Flask, render_template, request, redirect, url_for, session, make_response
import sqlite3
import os
from io import StringIO
import csv
from functools import wraps
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)
app.secret_key = 'supersecretkey'

# 로거 설정
if not app.debug:
    handler = RotatingFileHandler('flask.log', maxBytes=10000, backupCount=1)
    handler.setLevel(logging.INFO)
    formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')
    handler.setFormatter(formatter)
    app.logger.addHandler(handler)

def init_db():
    with sqlite3.connect('visitors.db') as conn:
        conn.execute('''
            CREATE TABLE IF NOT EXISTS visitors (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                affiliation TEXT NOT NULL,
                contact TEXT NOT NULL,
                visit_date TEXT NOT NULL,
                purpose TEXT NOT NULL
            )
        ''')
        conn.commit()

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'logged_in' not in session:
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        app.logger.info(f"Form data: {request.form}")
        privacy_agreement = request.form.get('privacy_agreement')
        app.logger.info(f"Privacy agreement: {privacy_agreement}")

        if privacy_agreement == 'agree':
            name = request.form['name']
            affiliation = request.form['affiliation']
            contact = request.form['contact']
            visit_date = request.form['visit_date']
            purpose = request.form['purpose']

            app.logger.info(f"Received data: {name}, {affiliation}, {contact}, {visit_date}, {purpose}")

            try:
                with sqlite3.connect('visitors.db') as conn:
                    cur = conn.cursor()
                    cur.execute(
                        'INSERT INTO visitors (name, affiliation, contact, visit_date, purpose) VALUES (?, ?, ?, ?, ?)',
                        (name, affiliation, contact, visit_date, purpose))
                    conn.commit()
                    visitor_id = cur.lastrowid
                    app.logger.info(f"New visitor added with ID: {visitor_id}")
            except sqlite3.Error as e:
                app.logger.error(f"Database error: {e}")
            except Exception as e:
                app.logger.error(f"Exception in query: {e}")

            return redirect(url_for('thank_you', visitor_id=visitor_id))
    return render_template('index.html')

@app.route('/thank_you/<int:visitor_id>')
def thank_you(visitor_id):
    app.logger.info(f"Thank you page accessed with ID: {visitor_id}")
    with sqlite3.connect('visitors.db') as conn:
        cur = conn.cursor()
        cur.execute('SELECT * FROM visitors WHERE id = ?', (visitor_id,))
        visitor = cur.fetchone()
    return render_template('thank_you.html', visitor=visitor)

@app.route('/retrieve', methods=['GET', 'POST'])
def retrieve():
    visitors = []
    if request.method == 'POST':
        name = request.form['name']
        contact = request.form['contact']

        app.logger.info(f"Retrieve request for Name: {name}, Contact: {contact}")

        with sqlite3.connect('visitors.db') as conn:
            cur = conn.cursor()
            cur.execute('SELECT * FROM visitors WHERE name = ? AND contact = ?', (name, contact))
            visitors = cur.fetchall()

    return render_template('retrieve.html', visitors=visitors)

@app.route('/download')
@login_required
def download():
    with sqlite3.connect('visitors.db') as conn:
        cur = conn.cursor()
        cur.execute('SELECT * FROM visitors')
        result = cur.fetchall()

    si = StringIO()
    cw = csv.writer(si)
    cw.writerow(['ID', 'Name', 'Affiliation', 'Contact', 'Visit Date', 'Purpose'])
    cw.writerows(result)

    response = make_response(si.getvalue())
    response.headers['Content-Disposition'] = 'attachment; filename=visitors.csv'
    response.headers['Content-type'] = 'text/csv; charset=utf-8-sig'
    return response

@app.route('/mgmt_web')
@login_required
def mgmt_web():
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 5, type=int)
    offset = (page - 1) * per_page

    with sqlite3.connect('visitors.db') as conn:
        cur = conn.cursor()
        cur.execute('SELECT COUNT(*) FROM visitors')
        total = cur.fetchone()[0]

        cur.execute('SELECT * FROM visitors LIMIT ? OFFSET ?', (per_page, offset))
        visitors = cur.fetchall()

    pagination = {
        'total': total,
        'per_page': per_page,
        'current_page': page,
        'total_pages': (total + per_page - 1) // per_page
    }

    return render_template('mgmt_web.html', visitors=visitors, pagination=pagination)

@app.route('/logs')
@login_required
def logs():
    log_file_path = 'flask.log'
    logs = ''
    if os.path.exists(log_file_path):
        with open(log_file_path, 'r') as log_file:
            logs = log_file.read()
    return render_template('logs.html', logs=logs)

@app.route('/delete_visitor/<int:visitor_id>')
@login_required
def delete_visitor(visitor_id):
    with sqlite3.connect('visitors.db') as conn:
        cur = conn.cursor()
        cur.execute('DELETE FROM visitors WHERE id = ?', (visitor_id,))
        conn.commit()
    return redirect(url_for('mgmt_web'))

@app.route('/edit_visitor/<int:visitor_id>', methods=['GET', 'POST'])
@login_required
def edit_visitor(visitor_id):
    if request.method == 'POST':
        name = request.form['name']
        affiliation = request.form['affiliation']
        contact = request.form['contact']
        visit_date = request.form['visit_date']
        purpose = request.form['purpose']

        with sqlite3.connect('visitors.db') as conn:
            cur = conn.cursor()
            cur.execute(
                'UPDATE visitors SET name = ?, affiliation = ?, contact = ?, visit_date = ?, purpose = ? WHERE id = ?',
                (name, affiliation, contact, visit_date, purpose, visitor_id))
            conn.commit()
        return redirect(url_for('mgmt_web'))

    visitor = None
    with sqlite3.connect('visitors.db') as conn:
        cur = conn.cursor()
        cur.execute('SELECT * FROM visitors WHERE id = ?', (visitor_id,))
        visitor = cur.fetchone()

    return render_template('edit_visitor.html', visitor=visitor)

@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username == 'admin' and password == 'password':  # 이 부분을 실제 환경에서는 강력한 인증 방식으로 변경 필요
            session['logged_in'] = True
            app.logger.info(f'Admin {username} logged in')
            return redirect(url_for('mgmt_web'))
        else:
            error = 'Invalid credentials. Please try again.'
            app.logger.warning(f'Failed login attempt with username: {username}')
    return render_template('login.html', error=error)

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    return redirect(url_for('index'))

if __name__ == '__main__':
    init_db()
    app.run(debug=True)

HTML 템플릿 작성

index.html

방문자 등록과 등록정보 확인 등에 필요한 index.html을 작성합니다.

개인정보처리방침 같은건 필요한 대로 수정하시기 바랍니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>방문자 등록</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <script>
        function checkAgreement() {
            var agree = document.querySelector('input[name="privacy_agreement"]:checked');
            var submitButton = document.getElementById('submit-button');
            submitButton.disabled = !(agree && agree.value === 'agree');
        }
    </script>
</head>
<body>
    <div class="container">
        <h1>방문자 등록</h1>
        <form action="/" method="post">
            <div class="form-container">
                <div class="form-left">
                    <div class="privacy">
                        <h3>개인정보 수집 및 이용 동의서</h3>
                        <textarea readonly rows="15">
방문자 등록 개인정보 수집 및 이용 동의서

[기관명] (이하 "기관")은 방문자 등록을 위해 다음과 같이 개인정보를 수집 및 이용하고자 합니다. 아래의 내용을 자세히 읽으신 후, 동의 여부를 체크해 주시기 바랍니다.

1. 수집하는 개인정보 항목
- 이름
- 소속
- 연락처(휴대전화 번호)
- 방문 일시
- 방문 목적

2. 개인정보의 수집 및 이용 목적
기관은 수집한 개인정보를 다음과 같은 목적으로 이용합니다.
- 방문자 관리: 방문자의 신원을 확인하고 방문 목적을 기록하여 안전하고 원활한 방문이 이루어지도록 합니다.
- 출입 관리: 방문자의 출입을 통제하고 비상 상황 시 신속히 대응할 수 있도록 합니다.
- 통계 분석: 방문자 통계를 분석하여 서비스 개선 및 보안 강화를 위한 자료로 활용합니다.

3. 개인정보의 보유 및 이용 기간
기관은 방문자의 개인정보를 수집 및 이용 목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. 단, 관련 법령에 따라 일정 기간 보관해야 하는 경우에는 법령에서 정한 기간 동안 보관합니다.
- 방문 기록: 1년

4. 개인정보의 제3자 제공
기관은 원칙적으로 방문자의 개인정보를 제3자에게 제공하지 않습니다. 다만, 다음의 경우에는 예외로 합니다.
- 방문자의 사전 동의를 받은 경우
- 법령의 규정에 의하거나, 수사 목적으로 법령에 정해진 절차와 방법에 따라 수사기관의 요구가 있는 경우

5. 개인정보의 안전성 확보 조치
기관은 방문자의 개인정보를 안전하게 관리하기 위해 다음과 같은 조치를 취하고 있습니다.
- 기술적 조치: 개인정보의 안전한 처리를 위해 비밀번호 설정, 암호화, 보안 프로그램 설치 등의 기술적 조치를 취하고 있습니다.
- 관리적 조치: 개인정보 보호를 위해 내부 관리 계획을 수립하고, 직원 교육을 실시하고 있습니다.
- 물리적 조치: 개인정보를 보관하는 시스템의 접근을 통제하고, 보안 시설을 갖추어 개인정보의 유출을 방지하고 있습니다.

6. 동의를 거부할 권리 및 동의를 거부할 경우의 불이익
방문자는 개인정보 제공에 대한 동의를 거부할 권리가 있습니다. 다만, 동의를 거부할 경우 방문자 등록이 불가능할 수 있으며, 이에 따른 불이익은 없습니다.

본인은 위와 같이 개인정보 수집 및 이용에 관한 내용을 충분히 이해하였으며, 이에 동의합니다.
                        </textarea>
                        <table class="radio-table">
                            <tr>
                                <td><label for="agree">동의합니다.</label></td>
                                <td><input type="radio" id="agree" name="privacy_agreement" value="agree" onclick="checkAgreement();"></td>
                            </tr>
                            <tr>
                                <td><label for="disagree">동의하지 않습니다.</label></td>
                                <td><input type="radio" id="disagree" name="privacy_agreement" value="disagree" onclick="checkAgreement();"></td>
                            </tr>
                        </table>
                    </div>
                </div>
                <div class="form-right">
                    <div class="form-group">
                        <label for="name">이름:</label>
                        <input type="text" id="name" name="name" required>
                    </div>

                    <div class="form-group">
                        <label for="affiliation">소속:</label>
                        <input type="text" id="affiliation" name="affiliation" required>
                    </div>

                    <div class="form-group">
                        <label for="contact">연락처:</label>
                        <input type="text" id="contact" name="contact" required pattern="010-?\d{3,4}-?\d{4}" title="연락처 형식: 010-1234-5678 또는 01012345678">
                    </div>

                    <div class="form-group">
                        <label for="visit_date">방문 일시:</label>
                        <input type="datetime-local" id="visit_date" name="visit_date" required>
                    </div>

                    <div class="form-group">
                        <label for="purpose">방문 목적:</label>
                        <input type="text" id="purpose" name="purpose" required>
                    </div>

                    <button type="submit" id="submit-button" disabled>등록하기</button>
                </div>
            </div>
        </form>
        <div class="link-container">
            <a href="{{ url_for('retrieve') }}">등록 정보 조회</a>
        </div>
    </div>
</body>
</html>

retrive.html

사용자가 자신의 등록정보를 확인할 수 있도록 ”retrive.html’을 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>등록 정보 조회</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <h1>등록 정보 조회</h1>
        <form action="/retrieve" method="post">
            <div class="form-group">
                <label for="name">이름:</label>
                <input type="text" id="name" name="name" required>
            </div>
            <div class="form-group">
                <label for="contact">연락처:</label>
                <input type="text" id="contact" name="contact" required pattern="010-?\d{3,4}-?\d{4}" title="연락처 형식: 010-1234-5678 또는 01012345678">
            </div>
            <button type="submit">조회하기</button>
        </form>
        {% if visitors %}
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>이름</th>
                    <th>소속</th>
                    <th>연락처</th>
                    <th>방문 일시</th>
                    <th>방문 목적</th>
                </tr>
            </thead>
            <tbody>
                {% for visitor in visitors %}
                <tr>
                    <td>{{ visitor[0] }}</td>
                    <td>{{ visitor[1] }}</td>
                    <td>{{ visitor[2] }}</td>
                    <td>{{ visitor[3] }}</td>
                    <td>{{ visitor[4] }}</td>
                    <td>{{ visitor[5] }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        {% elif error %}
        <p>{{ error }}</p>
        {% endif %}
        <div class="link-container">
            <a href="{{ url_for('index') }}">홈으로 돌아가기</a>
        </div>
    </div>
</body>
</html>

thank_you.html

등록정보를 재인시켜줄 수 있는 ‘thank_you.html’을 작성합니다.

카운트 다운 등을 통해서 더 멋지게 꾸밀 수도 있을 것 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>등록 완료</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <script>
        setTimeout(function() {
            window.location.href = "/";
        }, 10000);
    </script>
</head>
<body>
    <div class="container">
        <h1>등록이 완료되었습니다.</h1>
        <p>이름: {{ visitor[1] }}</p>
        <p>소속: {{ visitor[2] }}</p>
        <p>연락처: {{ visitor[3] }}</p>
        <p>방문 일시: {{ visitor[4] }}</p>
        <p>방문 목적: {{ visitor[5] }}</p>
        <p>10초 후에 메인 페이지로 돌아갑니다.</p>
        <p><a href="/">메인 페이지로 돌아가기</a></p>
    </div>
</body>
</html>

mgmt_web.html

등록정보 조회, 수정, 삭제와 로그를 볼 수 있는 관리페이지를 작성합니다.

세부 기능은 더 추가가 필요할 것 같고, 사실 인증절차도 더 추가해야 합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>관리 페이지</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <h1>관리 페이지</h1>
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>이름</th>
                    <th>소속</th>
                    <th>연락처</th>
                    <th>방문 일시</th>
                    <th>방문 목적</th>
                    <th>편집</th>
                    <th>삭제</th>
                </tr>
            </thead>
            <tbody>
                {% for visitor in visitors %}
                <tr>
                    <td>{{ visitor[0] }}</td>
                    <td>{{ visitor[1] }}</td>
                    <td>{{ visitor[2] }}</td>
                    <td>{{ visitor[3] }}</td>
                    <td>{{ visitor[4] }}</td>
                    <td>{{ visitor[5] }}</td>
                    <td><a href="{{ url_for('edit_visitor', visitor_id=visitor[0]) }}">편집</a></td>
                    <td><a href="{{ url_for('delete_visitor', visitor_id=visitor[0]) }}">삭제</a></td>
                </tr>
                {% endfor %}
            </tbody>
        </table>

        <div class="pagination">
            {% if pagination.current_page > 1 %}
            <a href="{{ url_for('mgmt_web', page=pagination.current_page - 1, per_page=pagination.per_page) }}">&laquo; 이전</a>
            {% endif %}
            <span>페이지 {{ pagination.current_page }} / {{ pagination.total_pages }}</span>
            {% if pagination.current_page < pagination.total_pages %}
            <a href="{{ url_for('mgmt_web', page=pagination.current_page + 1, per_page=pagination.per_page) }}">다음 &raquo;</a>
            {% endif %}
        </div>
        <a href="{{ url_for('download') }}"><h2>등록 정보 다운로드</h2></a>
        <h2>로그</h2>
        <a href="{{ url_for('logs') }}">로그 보기</a>
        <br>
        <a href="{{ url_for('logout') }}">로그아웃</a>
    </div>
</body>
</html>

login.html

관리자 페이지에 들어가기 위해 로그인 인증절차를 구현하는 login.html을 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>로그인</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <h1>관리자 로그인</h1>
        <form action="{{ url_for('login') }}" method="post">
            <div class="form-group">
                <label for="username">사용자 이름:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">비밀번호:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit">로그인</button>
        </form>
        {% if error %}
        <p>{{ error }}</p>
        {% endif %}
    </div>
</body>
</html>

logs.html

로그를 확인할 수 있는 페이지를 구현하기 위한 ‘logs.html’을 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>로그</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <h1>로그</h1>
        <div class="log-container">
            <pre>{{ logs }}</pre>
        </div>
        <a href="{{ url_for('mgmt_web') }}">관리 페이지로 돌아가기</a>
        <br>
        <a href="{{ url_for('logout') }}">로그아웃</a>
    </div>
</body>
</html>

edit_visitor.html

등록된 방문자 정보를 수정할 수 있는 기능을 구현하기 위해 ‘edit_visitor.html’을 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>방문자 정보 수정</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <h1>방문자 정보 수정</h1>
        <form action="{{ url_for('edit_visitor', visitor_id=visitor[0]) }}" method="post">
            <div class="form-group">
                <label for="name">이름:</label>
                <input type="text" id="name" name="name" value="{{ visitor[1] }}" required>
            </div>
            <div class="form-group">
                <label for="affiliation">소속:</label>
                <input type="text" id="affiliation" name="affiliation" value="{{ visitor[2] }}" required>
            </div>
            <div class="form-group">
                <label for="contact">연락처:</label>
                <input type="text" id="contact" name="contact" value="{{ visitor[3] }}" required>
            </div>
            <div class="form-group">
                <label for="visit_date">방문 일시:</label>
                <input type="datetime-local" id="visit_date" name="visit_date" value="{{ visitor[4] }}" required>
            </div>
            <div class="form-group">
                <label for="purpose">방문 목적:</label>
                <input type="text" id="purpose" name="purpose" value="{{ visitor[5] }}" required>
            </div>
            <button type="submit">수정하기</button>
        </form>
        <a href="{{ url_for('mgmt_web') }}">취소</a>
    </div>
</body>
</html>

실제 구동

파이썬으로 방문자 등록 웹 페이지 구현하기 결과,

실제 구동 테스트 결과 일단 작성한 대로 잘 작동합니다.

다만, 인증이나 기능 면에서 부족한 부분이 있는 것은 사실입니다. 예를 들어 특정 페이지에 대한 권한 검증이 충분히 이루어지지 않는 등..

사용할 환경이 다 다를 수 있기 때문에 각자의 환경에 맞춰 잘 수정해 나가면 될 것 같습니다.

특히, 인증 부분에서 상당히 취약하고 등록정보를 마음대로 수정할 수 있다는 점에서 정보보안에는 많은 개선사항이 있어야 할 것입니다.

따라서 폐쇄망 등에서 간편하게 사용할 것을 권장드립니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤