이전 내용
[파이썬] FastAPI - 메모 앱 프로젝트 15: 이메일 기능 보완
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 14: 비밀번호 찾기(수정)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 13: 아이디 찾기이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 12: 파일 분할하기
puppy-foot-it.tistory.com
메인 페이지 나누기
현재 메인 페이지에 내용이 너무 많아서 아래와 같이 잘린다.
따라서, 메인 페이지를 로그인 페이지와 회원가입 페이지로 나눠주고, 연결되어 있는 파일들의 로직도 수정해주려고 한다.
참고하는 레이아웃은 아래와 같다.
◆ login.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>마이 메모 앱</title>
<style>
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
.container {
max-width: 400px;
padding: 2rem;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin: 1rem;
width: 100%;
}
h1 {
font-size: 1.5rem;
color: #007bff;
margin-bottom: 2rem;
}
p {
margin-bottom: 2rem;
color: #666;
}
.form-group {
margin-bottom: 1rem;
width: 100%;
}
.form-group label {
margin-bottom: .5rem;
color: #888;
text-align: left;
display: block;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 5px;
width: 100%;
box-sizing: border-box;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 5px;
width: 100%;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #80bdff;
box-shadow: 0 0 0 2px rgba(0,123,255,.25);
}
.buttons button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
margin-top: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
box-sizing: border-box;
}
.buttons button:hover {
background-color: #0056b3;
}
.divider {
display: flex;
align-items: center;
text-align: center;
margin: 2rem 0;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: #ccc;
}
.divider:not(:empty)::before {
margin-right: .75em;
}
.divider:not(:empty)::after {
margin-left: .75em;
}
@media (max-width: 768px) {
.container {
width: 90%;
padding: 1.5rem;
}
h1 {
font-size: 1.25rem;
}
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet">
<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(body => ({
status: response.status, body: body})))
.then(result => {
if (result.status == 200) {
alert(result.body.message); // 로그인 성공 메시지를 팝업으로 표시
window.location.href = '/memos'; // 메모 페이지로 리다이렉트
} else {
throw new Error(result.body.detail || '로그인이 실패하였습니다.'); // 로그인 실패 시 에러 발생 (서버 제공 에러 또는 기본 메시지)
}
})
.catch((error) => {
console.error('Error:', error);
alert(error.message); // 에러 메시지를 팝업으로 표시
});
}
</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>
<p>
<a href="/signup" style="text-decoration: none; color:blue;">
회원 가입 </a>
</p>
<p>
<a href="/find_account" style="text-decoration: none; color:blue;">
아이디/비밀번호 찾기 </a>
</p>
</form>
<div class="divider">소셜 로그인</div>
<div class="buttons">
<button onclick="window.location.href='/auth/google/login'">Google 계정으로 로그인</button>
</div>
<div class="buttons">
<button onclick="window.location.href='/auth/kakao/login'">KAKAO 계정으로 로그인</button>
</div>
<div class="buttons" style="margin-bottom: 2rem;">
<button onclick="window.location.href='/auth/naver/login'">NAVER 계정으로 로그인</button>
</div>
</div>
</body>
</html>
◆ signup.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>마이 메모 앱</title>
<style>
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
.container {
max-width: 400px;
padding: 2rem;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin: 1rem;
width: 100%;
}
h1 {
font-size: 1.5rem;
color: #007bff;
margin-bottom: 2rem;
}
p {
margin-bottom: 2rem;
color: #666;
}
.form-group {
margin-bottom: 1rem;
width: 100%;
}
.form-group label {
margin-bottom: .5rem;
color: #888;
text-align: left;
display: block;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 5px;
width: 100%;
box-sizing: border-box;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 5px;
width: 100%;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #80bdff;
box-shadow: 0 0 0 2px rgba(0,123,255,.25);
}
.buttons button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
margin-top: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
box-sizing: border-box;
}
.buttons button:hover {
background-color: #0056b3;
}
.divider {
display: flex;
align-items: center;
text-align: center;
margin: 2rem 0;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: #ccc;
}
.divider:not(:empty)::before {
margin-right: .75em;
}
.divider:not(:empty)::after {
margin-left: .75em;
}
@media (max-width: 768px) {
.container {
width: 90%;
padding: 1.5rem;
}
h1 {
font-size: 1.25rem;
}
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet">
<script>
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(body => ({
status: response.status, body: body})))
.then(result => {
if (result.status == 200) {
alert(result.body.message); // 회원가입 성공 메시지를 팝업으로 표시
window.location.href = '/'; // 메인 페이지로 리다이렉트
} else {
throw new Error(result.body.detail || '회원가입이 실패하였습니다.'); // 회원가입 실패 시 에러 발생 (서버 제공 에러 또는 기본 메시지)
}
})
.catch((error) => {
console.error('Error:', error);
alert(error.message); // 에러 메시지를 팝업으로 표시
});
}
</script>
</head>
<body>
<div class="container">
<h1>마이 메모 앱 회원가입 페이지</h1>
<p>마이 메모 앱 회원가입을 환영합니다.</p>
<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 class="divider">소셜 로그인으로 간단하게 회원가입</div>
<div class="buttons">
<button onclick="window.location.href='/auth/google/login'">Google 계정으로 가입하기</button>
</div>
<div class="buttons">
<button onclick="window.location.href='/auth/kakao/login'">KAKAO 계정으로 가입하기</button>
</div>
<div class="buttons" style="margin-bottom: 2rem;">
<button onclick="window.location.href='/auth/naver/login'">NAVER 계정으로 가입하기</button>
</div>
<p>
<a href="/" style="text-decoration: none; color:blue;">
로그인 페이지로 돌아가기 </a>
</p>
</div>
</body>
</html>
main.py 수정
main.py 파일에서는 홈페이지 엔드포인트를 home.html이 아닌 login.html로 연결되도록 해주고,
signup.py 엔드포인트와 연결되는 라우터를 생성해 준다.
# 홈페이지
@app.get('/')
async def read_root(request: Request):
logger.info("홈페이지 요청 수신")
return templates.TemplateResponse('login.html', {"request": request})
# 회원 가입 페이지 엔드 포인트
@app.get('/signup', response_class=HTMLResponse)
async def sign_up(request: Request):
logger.info("회원가입 페이지 요청 수신")
return templates.TemplateResponse('signup.html', {"request": request})
테스트 및 수정하기
◆ 메인 페이지(로그인)
회원 가입 링크를 누르면 회원 가입 페이지로 잘 넘어간다.
마찬가지로, 아이디/비밀번호 찾기 페이지로 넘어가는 것도 이상없이 실행된다.
소셜 로그인(구글, 카카오, 네이버)과 로그아웃도 문제 없이 실행된다.
일반 계정 로그인 및 로그아웃도 잘 된다.
비밀번호를 변경하는 링크가 생략되어 있어 추가해 주는데, 계정정보를 찾는 페이지 링크와 비밀번호 변경 페이지 링크가 같은 줄에 나타나도록 login.html 을 수정해 준다.
<p>
<a href="/signup" style="text-decoration: none; color:blue;">
회원 가입 </a>
</p>
<div style="text-align: center;">
<a href="/find_account" style="text-decoration: none; color: blue;">아이디/비밀번호 찾기</a> |
<a href="/change_pw" style="text-decoration: none; color: blue;">비밀번호 변경하기</a>
</div>
비밀번호 변경 페이지로의 이동도 잘 된다.
◆ 회원 가입 페이지
기존 회원 가입 페이지의 경우, 비밀번호를 한 번만 입력하도록 되어 있기 때문에 비밀번호 확인 칸을 하나 더 만들어서 비밀번호를 확인하고, 일치할 경우 회원가입이 성공하는 로직을 추가해 준다.
◆ users_controller.py
users_controller.py 파일에서 회원 가입 라우터에 두 번의 비밀번호 입력이 서로 일치할 경우에만 회원 가입이 성공할 수 있는 조건문을 하나 더 추가해 준다.
# 비밀번호 확인 일치 검사
if signup_data.password != signup_data.password_confirm:
logger.warning("비밀번호가 일치하지 않습니다.")
raise HTTPException(status_code=400, detail="비밀번호가 서로 일치하지 않습니다.")
또한, 회원 가입이 잘 진행되어 새로운 사용자가 db에 잘 저장됐는지 확인하기 위해 log를 반환받을 수 있도록 보완해 준다.
# 모든 조건 만족 시
hashed_password = get_password_hash(signup_data.password) # 비밀번호 해시 기능
new_user = User(username=signup_data.username, email=signup_data.email, hashed_password=hashed_password)
db.add(new_user)
logger.info(f"{new_user.username} 사용자가 데이터베이스에 추가 되었습니다.") # 추가
try:
db.commit()
logger.info("데이터베이스에 새로운 사용자가 추가되었습니다.") # 추가
except Exception as e:
db.rollback() # 에러 발생 시 롤백
logger.error(f"회원 가입 오류: {e}") # 에러 내용 출력
raise HTTPException(status_code=500, detail="회원 가입 실패. 다시 시도해 주세요.")
db.refresh(new_user)
그런데, shemas.py 파일에 정의해둔 UserCreate 모델에는 password_confirm 이라는 인자가 없다. 따라서, 해당 모델에 password_confirm 이라는 인자를 추가해 준다.
◆ shemas.py
# 회원 가입 시 데이터 검증
class UserCreate(BaseModel):
username: str
email: str
password : str # 해시 전 패스워드
password_confirm: str # 추가
◆ signup.html
[HTML 부분]
비밀번호 칸 밑에 비밀번호 확인 칸 추가
<div class="form-group">
<label for="signup_password">비밀번호:</label>
<input type="password" id="signup_password" name="password" required>
</div>
<div class="form-group">
<label for="signup_password_confirm">비밀번호 확인:</label>
<input type="password" id="signup_password_confirm" name="password_confirm" required>
</div>
[자바스크립트 부분]
기존의 submitSignupForm() 함수에 비밀번호 확인하는 부분 추가
추가로, 회원 가입 후 Form에 입력한 모든 데이터가 초기화될 수 있도록 설정해 준다.
<script>
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'),
password_confirm: formData.get('password_confirm')
};
fetch('/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json().then(body => ({
status: response.status, body: body})))
.then(result => {
if (result.status == 200) {
alert(result.body.message); // 회원가입 성공 메시지를 팝업으로
document.getElementById('signupForm').reset(); // 비밀번호 변경 모든 칸 초기화
//window.location.href = '/'; // 메인 페이지로 리다이렉트
} else {
throw new Error(result.body.detail || '회원가입이 실패하였습니다.'); // 회원가입 실패 시 에러 발생 (서버 제공 에러 또는 기본 메시지)
}
})
.catch((error) => {
console.error('Error:', error);
alert(error.message); // 에러 메시지를 팝업으로 표시
});
}
</script>
다시 서버를 실행해서 회원 가입 테스트를 진행해보면 회원 가입 성공 메시지가 잘 뜨고,
DB에도 잘 반영된 것을 확인할 수 있다.
다음 내용
[파이썬] FastAPI - 메모 앱 프로젝트 17: 소셜 로그인 탈퇴
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 16: 메인 페이지 나누기이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 15: 이메일 기능 보완이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 14: 비밀번호
puppy-foot-it.tistory.com
'[파이썬 Projects] > <파이썬 웹개발>' 카테고리의 다른 글
[파이썬] FastAPI - 메모 앱 프로젝트 18(최종): 자동 로그아웃 (0) | 2025.05.19 |
---|---|
[파이썬] FastAPI - 메모 앱 프로젝트 17: 소셜 로그인 탈퇴 (0) | 2025.05.15 |
[파이썬] FastAPI - 메모 앱 프로젝트 15: 이메일 기능 보완 (0) | 2025.05.14 |
[파이썬] FastAPI - 메모 앱 프로젝트 14: 비밀번호 찾기(수정) (0) | 2025.05.13 |
[파이썬] FastAPI - 메모 앱 프로젝트 13: 아이디 찾기 (0) | 2025.05.13 |