본문 바로가기
IT

Flask - 애플리케이션(메모앱) 만들어보기 #6

by 고래(부와 성공) 2024. 11. 23.

앞에서 만들어본 메모앱은 프론트엔드 페이지를 거의 만들지 않고 curl 명령으로 테스트를 진행했었습니다.

 

여기서 프런트엔드 데이지를 개선 및 추가해서 메모앱을 웹에서 작동이 되도록 해보겠습니다.

 

프론트엔드 페이지는 HTML, CSS, 자바스크립트로 작성하고, Jinja2 이라 불리는 템플릿에 연동되어 동작할 수 있게 만들어 볼께요

 

개선 하고 추가할 파일은 templates 폴더 아래에 있는 memos.html 과 index.html 입니다.

 

1. index.html 작성하기

index.html 은 http://127.0.0.1:5000/과 같이 루트 라우트에서 보여지는 페이지입니다.

 

이 페이지에서 로그인 및 회원가입 기능을 구현하기 위해 아래와 같이 코드를 변경합니다.

 

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>온라인 메모 앱 v1.0</title>
    <style>
        body { font-family: Arial, Helvetica, sans-serif; }
        .container {
            width: 300px;
            margin: auto;
            border: 1px solid #ddd;
            padding: 20px;
        }
        .form-group {
            margin-bottom: 10px;
        }
        .form-group label, .form-group input {
            display: block;
            width: 100%;
        }
        .form-group input {
            padding: 5px;
            margin-top: 5px;
        }
        .buttons {
            display: flex;
            justify-content: space-between;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <h2>나의 메모 앱에 오신것을 환영해요</h2>
    <p> 이것은 온라인 메모장 앱입니다.</p>

    <form action="/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>
        <div class="buttons">
            <input type="submit" type="submit" value="로그인">
            <a href="/signup">회원가입</a>
        </div>
    </form>
</body>
</html>

 

 

2. memos.html 작성하기

또한 memos.html도 아래와 같이 작성합니다.

 

memos.html 은 로그인 한 후에 자신의 메모 리스트 보여지며, 이를 수정, 삭제하는 것은 물론 새로운 메모를 새로 추가하는 기능을 구현하는 페이지가 되겠습니다.

 

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>내 메모 관리 페이지</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" rel="stylesheet">
    <style>
        /* CSS 스타일 시작 */
        .container {
            margin-top: 20px;
            max-width: 800px;
        }

        .card {
            margin-bottom: 20px;
            border: none;
            box-shadow: 0 4px 8px rgba(0,0,0,.1);
            background-color: #fff;
        }

        .card-body {
            position: relative;
            padding: 10px;
        }

        .memo-title, .memo-content {
            width: 100%;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            background-color: #fff;
            padding: 10px;
        }

        .memo-title {
            font-size: 1.1rem;
        }

        .memo-content {
            min-height: 100px;
        }

        .edit-buttons {
            margin: 10px;
            text-align: right;
            margin-right: 0px;
            margin-bottom: 0px;
        }

        .edit-buttons .btn {
            background-color: #f8f9fa;
            border: none;
            border-radius: 5px;
            margin-left: 5px;
            padding: 5px 10px;
            color: #495056;
            transition: all 0.3s ease;
        }

        .edit-buttons .btn:hover {
            background-color: #e2e6ee;
            transform: scale(1.1);
        }

        .edit-buttons .btn-edit {
            background-color: #e74c3a;  
            color: #fff;
        }

        .edit-buttons .btn-edit:hover {
            background-color: #c0392a;  
        }

        .edit-buttons .btn-delete {
            background-color: #3498BB;  
            color: #fff;
        }

        .edit-buttons .btn-delete:hover {
            background-color: #2980BB;  
        }

        .btn-primary {
            background-color: #3f4644;  
            color: #fff;
        }

        .btn-primary:hover {
            background-color: #0056BB;  
        }

        .btn-block {
            display: block;
            width: 100%
        }

        .header-bar {
            background-color: #ff8066;
            padding: 10px 0;
            text-align: center;
            border-radius: 10px; 
            box-shadow: 0 4px 6px rgba(0,0,0,.1);  /* 그림자 effect */
            animation: slideDown 0.5s ease-out;
            margin: 10px;
            position: relative;
            display: flex; /* flexbox layout */
            justify-content: center;  /* 가로 중앙 정렬 */
            align-items: center; /* 세로 중앙 정렬 */
        }

        .header-item {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
        }

        .header-item:first-child {
            left: 20px;
        }

        .header-item:last-child {
            right: 20px;
        }

        .username-button, .logout-button {
            display: flex;
            align-items: center;
        }

        .username-button i, .logout-button i {
            margin-right: 5px;
        }

        .header-bar h1 {
            color: white;
            margin: 0;
            font-size: 1.3em;
            font-weight: bold;
            transition: all 0.3s ease-in-out;  /* 부드러운 효과 변화 */
        }

        .header-content {
            text-align: center;
        }

        .user-info {
            position: absolute;
            top: 10px;
            right: 20px;
            font-size: 0.9rem;
        }

        .logout-button {
            margin-left: 10px;
        }

        .btn-sm {
            padding: 0.15rem 0.5rem;
            font-size: .8rem;
            line-height: 1.5;
            border-radius: 0.2rem;
        }

        /* 슬라이드 다운 애니메이션 effect */
        @keyframes slideDown {
            from {
                transform: translateY(-100%);
                opacity: 0;
            }
            to {
                transform: translateY(0);
                opacity: 1;
            }
        }
    </style>
    <script>
        function createMemo() {
            let title = document.getElementById('new-title').value;
            let content = document.getElementById('new-content').value;
            
            fetch('/memos/create', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ title: title, content: content })
            })
            .then(response => response.json())
            .then(data => {
                console.log(data);
                window.location.reload(); // 페이지 새로 고침
            })
            .catch((error) => {
                console.error('Error:', error);
            });
        }    

        function toggleEdit(id) {
            var titleEl = document.getElementById('title-' + id);
            var contentEl = document.getElementById('content-' + id)
            var isReadOnly = titleEl.readOnly;

            titleEl.readOnly = !isReadOnly;
            contentEl.readOnly = !isReadOnly;

            if (!isReadOnly) {
                updateMemo(id);
            }
        }

        function updateMemo(id) {
            var title = document.getElementById('title-' + id).value;
            var content = document.getElementById('content-' + id).value;
            
            fetch('/memos/update/' + id, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ title: title, content: content})
            })
            .then(response => response.json())
            .then(data => {
                console.log(data);
                alert('메모가 업데이트되었습니다.');
            })
            .catch((error) => {
                console.error('Error:', error);
            })
        }

        function deleteMemo(id) {
            if (!confirm('메모를 정말로 삭제하시겠습니까?')) return;

            fetch('/memos/delete/' + id, {
                method: 'DELETE',
            })
            .then(response => response.json())
            .then(data => {
                console.log(data);
                window.location.reload(); // 페이지 새로고침
            })
            .catch((error) => {
                console.error('Error:', error);
            })
        }
    </script>
</head>

<body>
    <div class="container">
        <!-- 헤더바 추가 -->
        <div class="header-bar" >
            <div class="header-item">
                <a href="#" class="btn btn-sm btn-danger username-button">
                    <i class="fas fa-user"></i> {{ username }}
                </a>
            </div>
        </div>
        <h1>나의 메모</h1>
        <div class="header-item">
            <a href="/logout" class="btn btn-sm btn-danger logout-button">
                <i class="fas fa-sign-out-alt"></i> 로그아웃
            </a>
        </div>
        <div class="card">
            <div class="card-body">
                <input type="text" id="new-title" placeholder="새 메모 제목" class="form-control memo-title">
                <textarea id="new-content" placeholder="내용을 입력하세요" class="form-control memo-content"></textarea>
                <button onclick="createMemo()" class="btn btn-primary btn-block">메모 추가</button>
            </div>
        </div>

        {% for memo in memos %}
        <div class="card memo">
            <div class="card-body">
                <input type="text" id="title-{{ memo.id }}" value="{{ memo.title }}" class="form-control memo-title" readonly>
                <textarea id="content-{{ memo.id }}" class="form-control memo-content" readonly>{{ memo.content }}</textarea>
                <div class="edit-buttons">
                    <button onclick="toggleEdit({{ memo.id }})" class="btn btn-edit"><i class="fas fa-edit"></i></button>
                </div>
                <div class="edit-buttons">
                    <button onclick="deleteMemo({{ memo.id }})" class="btn btn-delete"><i class="fas fa-trash-alt"></i></button>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>
</body>
</html>

 

 

3. 테스트해보기

1) http://127.0.0.1:5000/ 로 접속해서 회원 가입하기

 

2) http://127.0.0.1:5000/ 로 재접속해서 로그인 하기

3) http://127.0.0.1:5000/memos에 접속해서 글쓰고 수정 삭제하기

 

 

 

4) 동일 페이지에서 수정하기

 

 

5) 삭제하기

파란색 휴지통을 클릭하면 다음과 같이 메시지를 띄웁니다.

 

 

 

=> 삭제 완료 되었네요

 

오늘은 여기까지 입니다.

 

감사합니다.