Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Tags
more
Archives
Today
Total
관리 메뉴

개발일기

flask#2 본문

python/flask

flask#2

kimjw7815 2025. 4. 1. 22:43
from flask import Flask, render_template, request, redirect, url_for, session
from functools import wraps
import time
import sqlite3

import bcrypt
from collections import defaultdict

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


# 로그인 시도 기록
login_attempts = defaultdict(list)

# 로그인 시도 횟수 제한
MAX_ATTEMPTS = 5
LOCK_TIME = 60 * 5  # 5분

###############################################################################
# 기타 함수들 ##################################################################

# login_required라는 데코레이터를 정의
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

# SQLite 연결을 위한 함수
def get_db_connection():
    conn = sqlite3.connect('requests.db')
    conn.row_factory = sqlite3.Row  # 결과를 딕셔너리 형식으로 반환
    return conn

# SQLite에 데이터를 저장하는 함수
def save_to_db(request_data):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute('''
        INSERT INTO requests (name, chk_info, language, sub_time, user_id)
        VALUES (?, ?, ?, ?, ?)
    ''', (request_data['name'], request_data['chk_info'], request_data['language'], request_data['sub_time'], request_data['user_id']))
    conn.commit()
    conn.close()

# SQLite에서 데이터를 불러오는 함수
def load_from_db(user_id):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM requests WHERE user_id = ?', (user_id,))
    requests = cursor.fetchall()
    conn.close()
    return requests

# 요청을 삭제하는 함수
def delete_request_from_db(user_id, sub_time):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute('DELETE FROM requests WHERE user_id = ? AND sub_time = ?', (user_id, sub_time))
    conn.commit()
    conn.close()

# 비밀번호 해싱 함수
def hash_password(password):
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())

# 비밀번호 검증 함수
def check_password(stored_hash, password):
    return bcrypt.checkpw(password.encode('utf-8'), stored_hash)

users = {
    "admin": hash_password("password123"),
    "user": hash_password("userpass")
}

###############################################################################
# render 함수들 ###############################################################

# 메인 화면
@login_required
@app.route('/', methods=['GET', 'POST'])
def home():
    if request.method == 'POST':
        # 전송된 데이터
        request_data = {
            'name': request.form.get('name'),
            'chk_info': request.form.get('chk_info'),
            'language': request.form.get('language'),
            'sub_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        }

        # 계정 id와 함께 요청 데이터를 세션에 저장
        request_data['user_id'] = session.get('user_id', 'anonymous')  # 로그인된 사용자 id가 없으면 'anonymous'

        # db에 저장
        save_to_db(request_data)

        # 세션에 전송 완료 메시지 추가
        session['message'] = f'전송이 완료되었습니다!\n{str(request_data)}'
        
        return redirect(url_for('home'))
    
    # 메시지 확인 및 반환
    message = session.pop('message', None)
    return render_template('index.html', session=session, message=message)

# 지금까지 보낸 요청 확인하기
@app.route('/history', methods=['GET', 'POST'])
@login_required
def history():
    user_id = session['user_id']  # 로그인된 사용자 아이디

    # SQLite에서 해당 사용자의 요청 데이터를 읽어옴
    conn = sqlite3.connect('requests.db')
    cursor = conn.cursor()

    cursor.execute('''SELECT * FROM requests WHERE user_id = ?''', (user_id,))
    requests = cursor.fetchall()  # 모든 요청 데이터 가져오기

    # 데이터는 튜플 형태로 반환되므로, 이를 딕셔너리로 변환
    requests = [{'name': row[1], 'chk_info': row[2], 'language': row[3], 'sub_time': row[4], 'user_id': row[5]} for row in requests]

    conn.close()  # 연결 종료

    return render_template('history.html', requests=requests)

# 회원가입
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')

        if username in users:
            return "이미 존재하는 사용자입니다."
        
        # 비밀번호 해싱
        hashed_password = hash_password(password)
        users[username] = hashed_password  # 사용자 이름과 해시된 비밀번호를 저장 (bytes로 저장)
        return redirect(url_for('login'))
    return render_template('register.html')



# 로그인
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')

        # 로그인 시도 기록
        if username in login_attempts:
            attempts = login_attempts[username]
            attempts = [t for t in attempts if t > time.time() - LOCK_TIME]  # 유효한 시도만 남김
            login_attempts[username] = attempts
            if len(attempts) >= MAX_ATTEMPTS:
                return "로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요."

        # 사용자 검증
        if username in users and check_password(users[username], password):
            session['user_id'] = username
            login_attempts[username] = []  # 로그인 성공 시 시도 기록 초기화
            return redirect(url_for('home'))
        
        # 로그인 실패 기록
        login_attempts[username].append(time.time())
        return "로그인 실패! 다시 시도하세요."
    return render_template('login.html')



# 로그아웃
@app.route('/logout', methods=['GET', 'POST'])
def logout():
    session.pop('user_id', None)
    return redirect(url_for('home'))

###############################################################################
# 기능 함수들 ##################################################################

# 요청 취소
@app.route('/cancel_request/<sub_time>', methods=['POST'])
@login_required
def cancel_request(sub_time):
    user_id = session['user_id']  # 로그인된 사용자 아이디

    # SQLite에서 해당 사용자와 요청 시간이 일치하는 데이터를 삭제
    conn = sqlite3.connect('requests.db')
    cursor = conn.cursor()

    # 삭제 쿼리 작성
    cursor.execute('''DELETE FROM requests WHERE user_id = ? AND sub_time = ?''', (user_id, sub_time))
    conn.commit()  # 변경 사항 저장
    conn.close()  # 연결 종료

    return redirect(url_for('history'))


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

요새는 GPT만 다룰줄 알아도 대부분의 코드를 쓸 수 있으니 정말 편하다.

작성 과정은 천천히 로그인/로그아웃 구현, session으로 로그인 여부 확인, 요청 기록(history) 확인 구현, csv로 데이터 저장->sqlite로 데이터 저장, 부트스트랩 적용.

template은 base.html을 만들고, 그걸 확장해서 index.html, login.html, register.html, history.hmtl을 만드는 식으로 구성했다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Website</title>
    <!-- 부트스트랩 CSS 링크 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <!-- 상단바 (NavBar) -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('home') }}">My Website</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    {% if 'user_id' in session %}
                        <li class="nav-item">
                            <span class="nav-link">{{ session['user_id'] }}</span>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('history') }}">요청 내역</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('logout') }}">로그아웃</a>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('login') }}">로그인</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('register') }}">회원가입</a>
                        </li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>

    <!-- 페이지 콘텐츠 -->
    <div class="container mt-4">
        {% block content %}{% endblock %}
    </div>

    <!-- 부트스트랩 JS 링크 -->
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script>
</body>
</html>

base.html에는 navbar, 즉 상단바와 관련된 내용을 넣었고.

{% extends "base.html" %}

{% block content %}
    <div class="container mt-4">
        <h2 class="mb-4">로그인</h2>
        
        <!-- 로그인 실패 메시지 출력 -->
        {% if message %}
            <div class="alert alert-danger">
                {{ message | e }}  <!-- XSS 방어를 위해 자동으로 이스케이프 처리 -->
            </div>
        {% endif %}

        <form method="POST">
            <div class="mb-3">
                <label for="username" class="form-label">아이디:</label>
                <input type="text" id="username" name="username" class="form-control" required>
            </div>
        
            <div class="mb-3">
                <label for="password" class="form-label">비밀번호:</label>
                <input type="password" id="password" name="password" class="form-control" required>
            </div>

            <button type="submit" class="btn btn-primary">로그인</button>
        </form>
        
        <p class="mt-3">아직 계정이 없으신가요? <a href="{{ url_for('register') }}">회원가입 페이지로 이동</a></p>
    </div>
{% endblock %}

거기에 extends 해서 넣을 내용을 정해놓는 방식.

그러면 이렇게 훌륭하게 만들어진다.

 

index.html
history.html
login.html

 

'python > flask' 카테고리의 다른 글

flask#1  (0) 2025.03.31