이전 내용
[파이썬] FastAPI - 메모 앱 프로젝트 4: 사용자별 메모 관리(feat. Almebic)
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 3: 사용자 인증이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 2: CRUD 구현이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 1: 초기 설정, DB 연동이전 내용 [
puppy-foot-it.tistory.com
프론트엔드 페이지 개선하기
home.html
프로젝트에 프론트엔드 페이지를 추가하여 웹에서 프로젝트를 확인할 수 있도록 한다. 프론트엔드 페이지는 HTML, CSS, 자바스크립트로 작성되며 Jinja2 템플릿 엔진과 연동된다.
templates 폴더 내에 생성된 home.html 페이지는 로그인 및 회원가입 기능을 제공하며, 사용자가 입력한 정보는 /login 및 /signup 엔드포인트로 전송된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>마이 메모 앱</title>
<style>
body { font-family: Arial, 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>
<script>
function submitLoginForm(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
username: formData.get('username'),
password: formData.get('password')
};
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
alert(data.message); // 로그인 응답 메시지를 팝업으로 표시
})
.catch((error) => {
console.error('Error:', error);
alert('로그인 에러: ' + error); // 에러 메시지를 팝업으로 표시
});
}
function submitSignupForm(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password')
};
fetch('/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
alert(data.message); // 회원가입 응답 메시지를 팝업으로 표시
})
.catch((error) => {
console.error('Error:', error);
alert('회원가입 에러: ' + error); // 에러 메시지를 팝업으로 표시
});
}
</script>
</head>
<body>
<div class="container">
<h1>마이 메모 앱에 오신 것을 환영합니다!</h1>
<p>간단한 메모를 작성하고 관리할 수 있는 앱입니다.</p>
<form id="loginForm" onsubmit="submitLoginForm(event)">
<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">
<button type="submit">로그인</button>
</div>
</form>
<form id="signupForm" onsubmit="submitSignupForm(event)">
<div class="form-group">
<label for="signup_username">사용자 이름:</label>
<input type="text" id="signup_username" name="username" required>
</div>
<div class="form-group">
<label for="signup_email">이메일:</label>
<input type="email" id="signup_email" name="email" required>
</div>
<div class="form-group">
<label for="signup_password">비밀번호:</label>
<input type="password" id="signup_password" name="password" required>
</div>
<div class="buttons">
<button type="submit">회원가입</button>
</div>
</form>
</div>
</body>
</html>
[전체 구조 요약]
구역 | 역할 |
<head> | CSS(스타일)과 JavaScript 코드 |
<body> | 실제로 보이는 로그인/회원가입 폼 |
<form> | 사용자의 입력값을 서버에 전송하는 양식 |
JavaScript | fetch()로 FastAPI 서버에 데이터 전송 (AJAX 방식) |
[로그인 폼 분석]
1. HTML 부분: 사용자가 이름과 비밀번호를 입력하면, submitLoginForm(event)라는 JavaScript 함수가 실행
<form id="loginForm" onsubmit="submitLoginForm(event)">
...
<input type="text" id="username" name="username" required>
<input type="text" id="password" name="password" required>
...
</form>
태그 | 설명 |
<form> | 사용자 입력을 서버로 보내기 위한 태그 |
onsubmit | 폼을 제출할 때 실행할 JavaScript 함수 지정 |
<input> | 실제로 사용자에게 보여지는 입력창 |
name | 서버에 데이터를 보낼 때의 key 이름 |
required | 빈칸이면 제출 막기 |
<button type="submit"> | 폼을 제출하게 하는 버튼 |
코드 | 설명 |
<form id="loginForm" onsubmit="submitLoginForm(event)"> | 로그인 폼의 시작. onsubmit은 이 폼을 제출할 때 실행할 JavaScript 함수(submitLoginForm)를 지정함. |
<div class="form-group"> | 입력 요소들을 묶는 박스 (CSS로 스타일링에 사용). |
<label for="username">사용자 이름:</label> | 사용자에게 보여지는 "아이디"라는 라벨. for="username"은 아래 input과 연결되어 있음. |
<input type="text" id="username" name="username" required> | 사용자 이름 입력 칸. required는 필수 입력임을 뜻함. |
</div> | 사용자 이름 입력 그룹 끝. |
<div class="form-group"> | 비밀번호 입력 칸을 위한 그룹 시작. |
<label for="password">비밀번호:</label> | 사용자에게 보여지는 "비밀번호" 라벨. |
<input type="text" id="password" name="password" required> | 비밀번호 입력 칸. 실제 서비스에서는 type="password"로 바꾸는 게 일반적. |
</div> | 비밀번호 입력 그룹 끝. |
<div class="buttons"> | 버튼을 감싸는 박스 (버튼 간 간격 등 스타일 조정용). |
<button type="submit">로그인</button> | 이 버튼을 누르면 폼이 제출됨. 그리고 submitLoginForm() 함수가 실행됨. |
</div> | 버튼 그룹 끝. |
</form> | 로그인 폼 끝. 전체 입력 완료 후 제출 시 JavaScript 함수 실행됨. |
2. JavaScript 함수: 서버에 로그인 정보를 AJAX 방식으로 보낸다는 뜻
function submitLoginForm(event) {
event.preventDefault(); // 페이지 새로고침 막기
const formData = new FormData(event.target);
const data = {
username: formData.get('username'),
password: formData.get('password')
};
fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
alert(data.message); // 서버에서 온 응답 메시지
})
...
}
코드 | 설명 |
event.preventDefault(); | 기본 동작인 폼 전송 후 새로고침을 막음. AJAX 방식에서는 꼭 필요함. |
new FormData(event.target) | <form> 안에 있는 모든 입력값을 하나의 데이터로 모음. (자동으로 input name="" 기준으로 가져옴) |
formData.get('username') | 사용자가 입력한 값을 꺼냄. 예: input name="username"의 값 |
fetch('/login', {...}) | /login 경로로 **비동기 요청(서버 호출)**을 보냄. 즉, FastAPI 서버에 로그인 정보 보내는 것 |
body: JSON.stringify(data) | 데이터를 JSON 형태로 변환해서 서버에 보냄 (FastAPI는 JSON을 이해함) |
.then(response => response.json()) | 서버 응답을 받아서, 그걸 JSON으로 바꿈 |
alert(data.message) | FastAPI 서버가 보내준 메시지를 팝업(alert)으로 띄움 |
.catch(...) | 네트워크 오류 등 문제가 생겼을 때, 콘솔에 에러를 찍고 팝업 띄움 |
[예시 흐름]
- 사용자가 아이디, 비밀번호 입력 후 버튼 클릭
- 브라우저가 새로고침하지 않고 JavaScript가 입력값만 꺼냄
- fetch()를 통해 /login 주소로 POST 요청 전송
- FastAPI 서버에서 로그인 처리하고, 결과를 { "message": "로그인 성공!" } 이런 식으로 반환
- 그 메시지를 팝업으로 보여줌
개념 | 설명 |
FormData | form에서 데이터 추출 도구 |
fetch() | 서버와 데이터 주고받기 |
preventDefault() | 폼의 기본 새로고침 막기 |
.then() / .catch() | 서버 응답 처리 / 에러 처리 |
JSON.stringify() | JS 객체 → JSON 문자열 변환 |
프론트엔드 페이지 개선하기
memos.html
로그인 후 자신의 메모 리스트를 확인하고, 수정, 삭제 및 새로운 메모까지 추가할 수 있는 memos.html 파일을 개선한다.
이 HTML 파일은 FastAPI와 연동되는 메모 관리 웹 앱의 프론트엔드 화면이다. 메모 추가, 수정, 삭제 기능은 JavaScript에서 API 요청(fetch)을 통해 FastAPI 백엔드와 통신한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<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>
.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;
}
.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: #495057;
transition: all 0.3s ease;
}
.edit-buttons .btn:hover {
background-color: #e2e6ea;
transform: scale(1.1);
}
.edit-buttons .btn-edit {
background-color: #E74C3C;
color: #fff;
}
.edit-buttons .btn-edit:hover {
background-color: #C0392B;
}
.edit-buttons .btn-delete {
background-color: #3498DB;
color: #fff;
}
.edit-buttons .btn-delete:hover {
background-color: #2980B9;
}
.btn-primary {
background-color: #3F464D;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.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); /* 그림자 효과 */
animation: slideDown 0.5s ease-out; /* 슬라이드 다운 애니메이션 */
margin: 10px;
position: relative;
display: flex; /* 플렉스박스 레이아웃 적용 */
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; /* 로그아웃 버튼과 사용자 ID 사이의 간격 */
}
.btn-sm {
padding: 0.15rem 0.5rem;
font-size: .8rem;
line-height: 1.5;
border-radius: 0.2rem;
}
/* 슬라이드 다운 애니메이션 효과 */
@keyframes slideDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
<script>
function createMemo() {
var title = document.getElementById('new-title').value;
var content = document.getElementById('new-content').value;
fetch('/memos', {
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/' + 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/' + id, {
method: 'DELETE',
})
.then(response => response.json())
.then(data => {
console.log(data);
window.location.reload(); // 페이지 새로고침
})
.catch((error) => {
console.error('Error:', error);
});
}
function logout() {
fetch('/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
console.log(data);
window.location.href = '/'; // 로그아웃 후 홈페이지로 리다이렉트
})
.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>
<h1>나의 메모</h1>
<div class="header-item">
<button onclick="logout()" class="btn btn-sm btn-danger logout-button">
<i class="fas fa-sign-out-alt"></i> 로그아웃</button>
</div>
</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>
<button onclick="deleteMemo({{ memo.id }})" class="btn btn-delete"><i class="fas fa-trash-alt"></i></button>
</div>
</div>
</div>
{% endfor %}
</div>
</body>
</html>
[주요 구조 설명]
코드 | 설명 |
<!DOCTYPE html> | HTML5 문서임을 브라우저에 알린다. |
<html lang="ko"> | 문서의 언어를 한국어로 설정. |
<meta charset="UTF-8"> | 한글을 포함한 다양한 문자를 인코딩할 수 있게 UTF-8 설정. |
<link href="...bootstrap.min.css" rel="stylesheet"> | Bootstrap 프레임워크로 기본 디자인을 적용. |
<link href="...font-awesome.min.css" rel="stylesheet"> | 아이콘 폰트(Font Awesome)를 사용하기 위한 설정. |
<div class="header-bar"> | 상단 헤더 바로, 사용자 이름과 로그아웃 버튼이 포함. |
{{ username }} | Jinja2 문법으로 FastAPI에서 전달한 사용자 이름을 표시. |
<textarea id="new-content"...> | 새 메모의 내용을 입력하는 텍스트박스. |
onclick="createMemo()" | 버튼 클릭 시 createMemo() 함수 실행 → FastAPI로 POST 요청 보냄 |
{% for memo in memos %} | FastAPI에서 넘겨준 메모 리스트를 반복 렌더링 |
{{ memo.title }} | 메모의 제목을 HTML에 표시 |
toggleEdit({{ memo.id }}) | 수정 버튼 클릭 시 해당 메모를 편집 가능하도록 변경 |
deleteMemo({{ memo.id }}) | 삭제 버튼 클릭 시 FastAPI로 DELETE 요청을 보냄 |
[핵심 작동 방식]
항목 | 설명 |
FastAPI 역할 | 클라이언트가 보낸 요청(JSON)을 처리하고, HTML 렌더링(Jinja2) 또는 JSON 응답 반환 |
JavaScript 역할 | 사용자의 행동(메모 생성/수정/삭제)을 감지하고 FastAPI로 HTTP 요청 (fetch) 보냄 |
Jinja2 문법 | HTML 안에서 서버 데이터를 {{ 변수 }} 또는 {% 반복문 %} 등으로 출력 |
스타일(CSS) | Bootstrap + 직접 작성한 CSS로 레이아웃과 버튼 스타일 꾸밈 |
[주요 함수]
함수명 | 설명 | 주요 동작 |
createMemo() | 새로운 메모를 생성 | POST /memos 요청을 보내고, 성공 시 페이지를 새로고침. |
toggleEdit(id) | 특정 메모 수정 모드 토글 | readonly을 해제하고, 수정 완료 시 updateMemo(id)를 호출 |
updateMemo(id) | 메모 내용을 수정 | PUT /memos{id}로 수정 내용을 전송하고, 성공 시 알림 표시 |
deleteMemo(id) | 메모를 삭제 | 삭제 확인 후 DELETE /memos{id} 요청을 보내고, 성공 시 새로고침 |
logout() | 로그아웃 수행 | POST /logout 요청 후 / 페이지로 리디렉션 |
main.py 수정하기
memos.html 접속 시 username을 추가로 전달하기 위해 main.py 파일을 수정한다.
# 메모 조회
@app.get("/memos")
async def list_memos(request: Request, db: Session = Depends(get_db)):
# 사용자별 메모 관리
username = request.session.get("username")
if username is None:
raise HTTPException(status_code=401, detail="Not Authorized")
user = db.query(User).filter(User.username == username).first()
if user is None:
raise HTTPException(status_code=404, detail="User를 찾을 수 없습니다.")
memos = db.query(Memo).filter(Memo.user_id == user.id).all()
# 리스트 내포 사용하여 전체 정보 반환
return templates.TemplateResponse("memos.html", {
"request": request,
"memos": memos,
"username": username}) # 사용자 이름 추가
테스트
main.py 서버를 실행하고, 웹 브라우저를 열고 root('/) URL로 접속한다.
◆ 회원가입 테스트
계정이 성공적으로 생성되었다.
◆ 로그인 테스트
앞서 생성한 계정으로 로그인을 하여 '로그인 성공!' 메시지가 표시되면 로그인 기능이 잘 구현된 것이다.
만약, 계정 정보가 일치하지 않을 경우에는 에러 메시지(undefined) 가 뜬다.
◆ 메모 관리 테스트
(로그인 상태) http://127.0.0.1:8000/memos 주소로 이동하면 상단에 로그인한 사용자 이름이 표시되어있다.
1. 메모 생성 기능
2. 메모 수정 기능
하단의 버튼(빨간색 'V' 표시)을 눌러 수정이 가능하다.
3. 메모 삭제 기능
메모가 잘 삭제된 것을 확인할 수 있다.
◆ 로그아웃 테스트
상단의 로그아웃 버튼을 누르면 메인 페이지로 이동한다.
◆ 비로그인 상태에서 접근 테스트
로그아웃 후 로그인되지 않은 상태에서 /memos에 접근하려고 하면 승인되지 않았다는 메시지가 뜨며 접근이 불가하다.
[참고]
가장 빠른 풀스택을 위한 플라스크 & FastAPI
다음 내용
[파이썬] FastAPI - 메모 앱 프로젝트 6: 프론트엔드 페이지 개선
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 5: 웹페이지 개선이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 4: 사용자별 메모 관리(feat. Almebic)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 3: 사용
puppy-foot-it.tistory.com
'[파이썬 Projects] > <파이썬 웹개발>' 카테고리의 다른 글
[파이썬] FastAPI - 메모 앱 프로젝트 7: 보완(회원가입 등) (0) | 2025.05.10 |
---|---|
[파이썬] FastAPI - 메모 앱 프로젝트 6: 프론트엔드 페이지 개선 (0) | 2025.05.10 |
[파이썬] FastAPI - 메모 앱 프로젝트 4: 사용자별 메모 관리(feat. Almebic) (0) | 2025.05.09 |
[파이썬] FastAPI - 메모 앱 프로젝트 3: 사용자 인증 (0) | 2025.05.09 |
[파이썬] FastAPI - 메모 앱 프로젝트 2: CRUD 구현 (0) | 2025.05.09 |