[Flask] 회원 정보 수정, 탈퇴, 로그아웃

2025. 1. 23. 20:35Calender Website

0. 드롭다운

드롭다운에서 프로필 수정, 계정 정보 수정, 로그아웃을 선택할 수 있다.

[HTML]

<div class="dropdown">
    <img src="{{ url_for('static', filename='images/settings.png') }}" class="nav-picture" id="setting-button">
    <div class="dropdown-content" id="settingsMenu">
        <a href="{{ url_for('views.profile_setting') }}" id="profile_button">Profile</a>
        <a href="{{ url_for('views.account_setting') }}" id="account_button">Account</a>
        <a href="{{ url_for('api.logout') }}" id="logout_button">Logout</a>
    </div>
</div>

 

 

1. 프로필 정보 수정

[HTML]

<!DOCTYPE html>
<html>
<head>
    <title>Calendar</title>
    <link rel="stylesheet" href="/static/css/profile_setting.css">
    <script src="/static/js/profile_setting.js" defer></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
</head>
<body>
    <nav class="navigation-bar">
        <div class="nav-item left">
            <img src="{{ url_for('static', filename='images/left-arrow.png') }}" class="nav-picture" onclick="history.back();">
        </div>
        <div class="nav-item center">
            계정 정보
        </div>
    </nav>

    <form id="profile-setting-form" class="profile-setting-form">
        <div id="user-profile">
            <img src="{{ url_for('static', filename='images/profile/basic_profile.jpg' if user.image_path == ''
            else user.image_path) }}" id="profile-image" class="profile-image">
            <input type="file" name="user-image" id="image-input" style="display: none;" accept="image/*">
        </div>

        <div class="user-profile">
            <p>Name</p>
            <input type="text" name="user-name" class="text-input" value="{{user.username}}">
        </div>

        <div class="user-profile">
            <p>Description</p>
            <input type="text" name="user-description" class="text-input" value="{{user.description}}">
        </div>

        <div class="user-profile">
            <p>Birthday</p>
            <input type="date" max="9999-12-31" name="user-birth" class="text-input" value="{{ 'YYYY-MM-DD' if user.birth == None else user.birth }}">
        </div>

        <div class="user-profile">
            <p>Interests</p>
            <input type="text" name="user-interests" class="text-input" value="{{user.interest}}">
        </div>

        <div class="user-profile">
            <p>Work</p>
            <input type="text" name="user-work" class="text-input" value="{{user.work}}">
        </div>

        <div class="user-profile">
            <p>Location</p>
            <input type="text" name="user-location" class="text-input" value="{{user.location}}">
        </div>

        <div class="user-profile">
            <p>Website</p>
            <input type="text" name="user-website" class="text-input" value="{{user.website}}">
        </div>

        <div class="button-div">
            <button type="submit">Submit</button>
        </div>
    </div>
</body>
</html>

 

[Javascript]

1. 이미지 업로드

이미지 미리보기를 클릭하면 file input도 활성화되면서 파일 선택창이 열린다. 파일을 선택하면 해당 이미지로 미리보기가 바뀌게 된다.

// click image and open file select
document.getElementById('profile-image').onclick = function() {
    document.getElementById('image-input').click();
};

// select image file and update image
document.getElementById('image-input').addEventListener('change', function(event) {
    const file = event.target.files[0];
    if (file) {
        const reader = new FileReader();
        reader.onload = function(e) {
            document.getElementById('profile-image').src = e.target.result;
        };
        reader.readAsDataURL(file);
    }
});

 

2. 프로필 업로드

profile-setting-form이 제출되면 폼데이터를 Flask로 보낸다. 프로필 업데이트가 완료되면 Toastify로 알려준다.

// update user profile
document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('.profile-setting-form').addEventListener('submit', function(e) {
        e.preventDefault();
        const formData = new FormData(this);

        axios.post('/update_profile', formData)
        .then(function(response) {
            Toastify({
                text: response.data.message,
                duration: 2000,
                gravity: "top",
                position: "center",
                style: {
                    background: "black",
                    color: "white",
                    borderRadius: "10px"
                }
            }).showToast();
        })
        .catch(function(error) {
            console.error('Error: ', error);
        });
    });
});

 

[Flask]

current_user에서 사용자 객체를 불러온다. user.username = user_name 과 같이 객체에 새로운 데이터를 저장한 뒤 커밋해주면 프로필을 업데이트 할 수 있다. 이후 json 메세지를 전달하면 javascript에서 Toastify로 메세지를 보여준다.

@api.route('/update_profile', methods=['POST','GET'])
@login_required
def update_profile():
    user = current_user

    if request.method == 'POST':
        user_name = request.form['user-name']
        user_description = request.form['user-description']
        user_birth = request.form['user-birth']
        if (user_birth == ''):
            user_birth = None
        user_interests = request.form['user-interests']
        user_work = request.form['user-work']
        user_location = request.form['user-location']
        user_website = request.form['user-website']
        user_image = request.files['user-image']
        # 프로필 사진이 변경될 때만 db에 수정된 경로 저장
        if user_image.filename != '':
            file_name = secure_filename(user_image.filename)
            file_path = os.path.join('app/static/images/profile', file_name)
            user_image.save(file_path)
            # db에 저장할 경로는 static 이후부터
            db_file_path = f'images/profile/{file_name}'
            user.image_path = db_file_path

        user.username = user_name
        user.description = user_description
        user.birth = user_birth
        user.interest = user_interests
        user.work = user_work
        user.location = user_location
        user.website = user_website
        
        db.session.commit()

        return jsonify ({
            'message': '프로필이 업데이트되었습니다.',
        })
    return render_template('proflie_setting.html')

 

2. 계정 정보 수정

이메일 수정

 

[HTML]

    <div class="user-account" onclick="openPopup('#email-popup')">
        <p>이메일</p>
        <div class="input-div">
            <p id="email-result">{{ user.email }}</p>
            <img src="{{ url_for('static', filename='images/right-arrow.png') }}" class="open-popup">
        </div>
    </div>

 

각 div를 클릭하면 openPopup 함수를 통해 팝업창이 뜬다. 팝업창에 이메일을 변경할 수 있는 폼이 있다. 

    <div class="modal-overlay" id="modal-overlay"></div>
    <div id="email-popup" class="popup">
        <h3>이메일을 변경합니다.</h3>
        <form id="email-form" class="popup-form" >
            <input type="email" id="user-email" name="user-email" placeholder="Email" required>
            <div class="popup-button-div">
                <button type="button" class="close-popup">취소</button>
                <button type="submit">변경</button>
            </div>
        </form>
    </div>

 

[Javascript]

div를 클릭하면 팝업창이 열린다. modal-overlay는 배경을 회색으로 처리하여 깔끔하게 만들 수 있다.

// open password popoup
function openPopup(divID) {
    document.querySelector('.modal-overlay').style.display = "block";
    document.querySelector(divID).style.display = "block";
}

 

취소 버튼을 누르거나 modal-overlay를 클릭하면 팝업창이 닫힌다. 이 때 팝업창은 이메일, 비밀번호, 전화번호 총 3개가 있으므로 단순히 querySelector로 하면 제대로 동작하지 않으므로 querySelectorAll로 모든 팝업창을 선택한 다음 forEach로 각 노드에 대하여 처리해야 한다.

const closePopup = () => {
    document.querySelector('.modal-overlay').style.display = "none";
    const popupElements = document.querySelectorAll('.popup');
    popupElements.forEach(popup => {
        popup.style.display = "none";
    })
    const popupFormElements = document.querySelectorAll('.popup-form');
    popupFormElements.forEach(popupForm => {
        popupForm.reset();
    })
};

document.querySelectorAll('.close-popup').forEach(button => {
    button.addEventListener('click', closePopup);
})
document.querySelector('.modal-overlay').addEventListener('click', closePopup);

 

이메일을 업데이트 하기 전에 정규표현식을 통해 이메일 형식이 유효한지 테스트한다. 만약 형식이 맞지 않다면 메세지를 띄운 다음 종료하고, 유효하다면 Flask와 AXIOS 통신을 통해 이메일을 등록하고 응답이 오면 html에 이메일을 업데이트하고 Toastify를 띄운다.

// update email
document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('#email-form').addEventListener('submit', function(e) {
        e.preventDefault();
        const formData = new FormData(this);

        const userEmail = document.querySelector('#user-email').value;
    
        // 이메일 형식 검증을 위한 정규표현식
        const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
    
        if (!emailRegex.test(userEmail)) {
            showToastify("올바른 이메일 형식이 아닙니다.");
            return
        }
    
        axios.post('/change_email', formData)
        .then(function(response) {
            showToastify(response.data.message);
            if (response.data.available) {
                document.getElementById('email-result').textContent = response.data.email;
                closePopup();
            }
        })
        .catch(function(error) {
            console.error('에러 발생: ', error);
        });
    });
});

 

(+ showToastify 함수)

// Toastify
function showToastify(message) {
    Toastify({
        text: message,
        duration: 2000,
        gravity: "top",
        position: "center",
        style: {
            background: "black",
            color: "white",
            borderRadius: "10px"
        }
    }).showToast();
}

 

[Flask]

register 함수와 마찬가지로 이메일이 중복되는지 먼저 테스트한다. 만약 중복된다면 available을 False로 한 뒤 종료한다.

이메일이 중복되지 않으면 current_user.email에 등록하고 커밋한 다음, available을 True로 하여 응답한다.

@api.route('/change_email', methods=['POST','GET'])
@login_required
def change_email():

    if request.method == 'POST':
        user_email = request.form['user-email']

        userExist = User.query.filter_by(email=user_email).first()
        if userExist:
            return jsonify({
                'available' : False,
                'message' : "이미 등록된 이메일입니다.",
            })
        else:
            current_user.email = user_email
            db.session.commit()
            return jsonify({
                'available' : True,
                'message' : '이메일 주소를 새로 등록하였습니다.',
                'email' : user_email,
            })
    return render_template('account_setting.html')

 

비밀번호 수정

[HTML]

팝업창 오픈 버튼

    <div class="user-account" onclick="openPopup('#password-popup')">
        <p>비밀변호 변경</p>
        <div class="input-div">
            <img src="{{ url_for('static', filename='images/right-arrow.png') }}" class="open-popup">
        </div>
    </div>

 

팝업창

    <div class="modal-overlay" id="modal-overlay"></div>
    <div id="password-popup" class="popup">
        <h3>비밀번호를 변경합니다.</h3>
        <form id="password-form" class="popup-form" >
            <input type="password" name="current-password" placeholder="현재 비밀번호" required>
            <input type="password" name="new-password" placeholder="새 비밀번호" required>
            <input type="password" name="confirm-password" placeholder="새 비밀번호 확인" required>
            <div class="popup-button-div">
                <button type="button" class="close-popup">취소</button>
                <button type="submit">변경</button>
            </div>
        </form>
    </div>

 

[Javascript]

openPopup, closePopup 함수는 동일하다. 이메일과 마찬가지로 AXIOS 통신을 하고 응답을 받으면 showToastify 해준다.

// update password
document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('#password-form').addEventListener('submit', function(e) {
        e.preventDefault();
        const formData = new FormData(this);

        axios.post('/change_password', formData)
        .then(function(response) {
            showToastify(response.data.message);
            if (response.data.flag) {
                closePopup();
            }
        })
        .catch(function(error) {
            console.error('Error: ', error);
        });
    });
});

 

[Flask]

비밀번호 유효성 검사를 수행한다. 새 비밀번호와 새 비밀번호가 동일한지 확인하고 동일하지 않으면 flag를 false로 하고 종료한다. 그 다음 db에 저장된 비밀번호 해시값과 새 비밀번호를 check_password_hash로 비교한 다음 동일하면 user.password에 해시값으로 저장하고 커밋한다.

@api.route('/change_password', methods=['POST','GET'])
@login_required
def change_password():
    user = current_user

    if request.method == 'POST':
        current_password = request.form['current-password']
        new_password = request.form['new-password']
        confirm_password = request.form['confirm-password']

        if new_password != confirm_password:
            return jsonify({
                'flag' : False,
                'message' : '새 비밀번호와 새 비밀번호 확인이 다릅니다.',
            })
        
        if check_password_hash(user.password, current_password):
            user.password = generate_password_hash(new_password)
            db.session.commit()
            return jsonify({
                'flag' : True,
                'message' : '성공적으로 비밀번호를 변경하였습니다.',
            })
        
        else:            
            return jsonify({
                'flag' : False,
                'message' : '현재 비밀번호가 다릅니다.',
            })

    return render_template('account_setting.html')

 

Phone Number는 이메일과 동일하므로 생략한다.

 

3. 회원 탈퇴

[HTML]

    <form class="delete-account-form" action="/delete_account" method="post">
        <button type="submit" id="delete-account-button">계정 삭제</button>
    </form>

[Javascript]

폼을 제출하여 회원 탈퇴를 진행하기 전에 confirm 메세지를 통해 정말로 탈퇴할 것인지 확인한다.

// delete account
document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('.delete-account-form').addEventListener('submit', function(e){
        if (!confirm('정말로 계정을 탈퇴하시겠습니까?')) {
            e.preventDefault();
        }
    });
});

[Flask]

current_user를 db.session.delete로 세션에서 삭제하고 커밋한다음, 로그아웃도 한다.

@api.route('/delete_account', methods=['POST'])
@login_required
def delete_account():
    user = current_user
    db.session.delete(user)
    db.session.commit()
    logout_user()
    return redirect(url_for('views.welcome'))

 

 

4. 로그아웃

[HTML]

url_for을 사용하여 바로 파이썬으로 넘어간다.

<a href="{{ url_for('api.logout') }}" id="logout_button">Logout</a>

 

[Flask]

로그아웃 이후 welcome 페이지로 돌아가 회원가입, 로그인 중 선택한다.

@api.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('views.welcome'))