프로젝트를 진행하면서 났던 여러 문제들 및 이슈들
dj-rest-auth 를 이용해서 이메일 인증 기능을 구현했는데, 받은 이메일 링크를 눌러도 인증이 안 되는 이슈가 있었다. postman 으로 보내봐도 nonfields 에러가 뜨길래 무엇인지 봤는데 답은 의외로 간단했다. 링크 자체가 문제였던 것이다.
나는 이메일 인증의 ConfirmRegisterView - 이메일 인증을 할 시 받는 유저의 필드와 관련된 클래스 - 를 커스텀한 뒤
accounts/views.py
class CustomRegisterView(RegisterView):
serializer_class = CustomRegisterSerializer
permission_classes = [permissions.AllowAny]
class ConfirmEmailView(APIView):
permission_classes = [AllowAny]
def get(self, *args, **kwargs):
self.object = confirmation = self.get_object()
confirmation.confirm(self.request)
# A React Router Route will handle the failure scenario
return HttpResponseRedirect("accounts/") # 인증실패 # 인증성공
def get_object(self, queryset=None):
key = self.kwargs["key"]
email_confirmation = EmailConfirmationHMAC.from_key(key)
if not email_confirmation:
if queryset is None:
queryset = self.get_queryset()
try:
email_confirmation = queryset.get(key=key.lower())
except EmailConfirmation.DoesNotExist:
# A React Router Route will handle the failure scenario
return HttpResponseRedirect("accounts/") # 인증실패 # 인증실패
return email_confirmation
def get_queryset(self):
qs = EmailConfirmation.objects.all_valid()
qs = qs.select_related("email_address__user")
return qs
조금 더 모양있는 이메일을 보내기 위해 템플릿도 커스텀했었는데, verify 버튼의 href 링크가 이상했던 것이다.
templates/account/email/email_confirmation_signup_message.html
{% load account %}
{% load i18n %}
{% block content %}
{% autoescape off %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Verify Email</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
/**
* Google webfonts. Recommended to include the .woff version for cross-client compatibility.
*/
@media screen {
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v10/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff) format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v10/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff) format('woff');
}
}
body,
table,
td,
a {
-ms-text-size-adjust: 100%;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/**
* Remove extra space added to tables and cells in Outlook.
*/
/**
* Better fluid images in Internet Explorer.
*/
img {
-ms-interpolation-mode: bicubic;
}
/**
* Remove blue links for iOS devices.
*/
a[x-apple-data-detectors] {
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
color: inherit !important;
text-decoration: none !important;
}
/**
* Fix centering issues in Android 4.4.
*/
div[style*="margin: 16px 0;"] {
margin: 0 !important;
}
body {
width: 100% !important;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
/**
* Collapse table borders to avoid space between cells.
*/
table {
border-collapse: collapse !important;
}
a {
color: #1a82e2;
}
img {
height: auto;
line-height: 100%;
text-decoration: none;
border: 0;
outline: none;
}
</style>
</head>
<script src="https://gist.github.com/mrron313/8348a68b90297eb077723929de31540c.js"></script>
<body style="background-color: #e9ecef;">
<!-- start preheader -->
<div class="preheader"
style="display: none; max-width: 0; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #fff; opacity: 0;">
회원가입을 해주셔서 감사합니다. R.AI 는 딥러닝 모델을 이용한 복약 도우미 사이트입니다.
</div>
<!-- end preheader -->
<!-- start body -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<!-- start logo -->
<tr>
<td align="center" bgcolor="#e9ecef">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="http://127.0.0.1:5500/main.html" target="_blank" style="display: inline-block;">
<img src="https://cdn-icons-png.flaticon.com/128/840/840729.png" alt="Logo" border="0"
width="48" style="display: block; width: 48px; max-width: 48px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- end logo -->
<!-- start hero -->
<tr>
<td align="center" bgcolor="#e9ecef">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td align="left" bgcolor="#ffffff"
style="padding: 36px 24px 0; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; border-top: 3px solid #d4dadf;">
<h1
style="margin: 0; font-size: 32px; font-weight: 700; letter-spacing: -1px; line-height: 48px;">
이메일 인증</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- end hero -->
<!-- start copy block -->
<tr>
<td align="center" bgcolor="#e9ecef">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<!-- start copy -->
<tr>
<td align="left" bgcolor="#ffffff"
style="padding: 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px;">
<p style="margin: 0;">이메일 인증을 위해 아래의 버튼을 눌러주세요</p>
</td>
</tr>
<!-- end copy -->
<!-- start button -->
<tr>
<td align="left" bgcolor="#ffffff">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td align="center" bgcolor="#ffffff" style="padding: 12px;">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td align="center" bgcolor="#1a82e2" style="border-radius: 6px;">
<a href="{{ activate_url }}" target="_blank"
style="display: inline-block; padding: 16px 36px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 16px; color: #ffffff; text-decoration: none; border-radius: 6px;">Verify</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- end button -->
<!-- start copy -->
<tr>
<td align="left" bgcolor="#ffffff"
style="padding: 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px;">
<p style="margin: 0;">만약 버튼이 눌리지 않는다면 이 링크를 눌러주세요 : </p>
<p style="margin: 0;"><a href="{{ activate_url }}" target="_blank">링크</a></p>
</td>
</tr>
<!-- end copy -->
<!-- start copy -->
<tr>
<td align="left" bgcolor="#ffffff"
style="padding: 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; border-bottom: 3px solid #d4dadf">
<p style="margin: 0;">Cheers,<br> FPN</p>
</td>
</tr>
<!-- end copy -->
</table>
</td>
</tr>
<!-- end copy block -->
<!-- start footer -->
<tr>
<td align="center" bgcolor="#e9ecef" style="padding: 24px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<!-- start permission -->
<tr>
<td align="center" bgcolor="#e9ecef"
style="padding: 12px 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
<p style="margin: 0;">You received this email because we received a request for change
password for your account. If you didn't request change password you can safely delete
this email.</p>
</td>
</tr>
<!-- end permission -->
</table>
</td>
</tr>
<!-- end footer -->
</table>
<!-- end body -->
</body>
</html>
{% endblocktrans %}
{% endautoescape %}
{% endblock %}
여기서 버튼의 <a href="{{ activate_url }}" target="_blank"
처음에 저 href 를 localhost:5500/login.html 로 설정해서 이메일 인증 과정을 건너뛰고 바로 login.html 화면으로 연결되었던 것이다.
dj-rest-auth 에서 설정한 url 을 통해 확인을 한 뒤 > return HttpResponseRedirect("accounts/") > views.py 의 클래스를 통해 login.html 의 과정이 맞다.
이건 예외처리를 1 개만 해둬서 생긴 오류이다. manual.html 로 이동해야 하는 경우가 모델이 인식되지 않았을 경우 / 인식은 되었으나 class 에 없는 경우 두 가지인데 그 두 가지 중 한 가지만 프론트에서 예외처리를 해 다른 예외가 생겼을 경우 manual.html 로 넘어가지 않는 오류였다. DrugCreate.js 에서 두 경우 모두 manual.html 로 넘어가게 설정해주었다.
async function DrugCreate() {
const input = document.getElementById("upload_file");
const loadingElement = document.getElementById("load");
const accessToken = localStorage.getItem("access");
const storage = JSON.parse(localStorage.getItem("payload"))
const user_id = storage['user_id']
console.log(user_id)
console.log(accessToken)
if (input) {
img = input.files[0];
console.log(img);
const formData = new FormData();
formData.append('img', img);
console.log(formData)
loadingElement.style.display = 'block';
const response = await fetch(`http://127.0.0.1:8000/drugs/create/${user_id}`, {
headers: {
"Authorization": "Bearer " + accessToken,
},
method: 'POST',
body: formData
}).then((res) => {
return res.json(); //Promise 반환
})
.then((json) => {
console.log(json); // 서버에서 주는 json데이터가 출력 됨
loadingElement.style.display = 'none';
if (json['message'] == "인식할 수 없습니다. 정보를 직접 입력해주세요" || json['message'] == '등록되어 있지 않은 알약입니다. 정보를 직접 입력해주세요') { // 이 부분 예외 처리
console.log(json['message'])
alert(json['message']);
location.href = 'manual.html';
}
else {
alert(json['message'])
location.reload();
}
});
}
else {
loadingElement.style.display = 'none';
alert("사진은 필수 항목입니다.");
location.reload();
}
}
create.html 에서 약을 등록하면 user.drugslist 에 drug object 가 등록이 되고 출력이 되는데, drug_id 로 출력이 되어 이름으로 출력하고 싶어졌다.
drugs/views.py
try:
drug = Drug.objects.get(name=drug_name)
print("존재하는 약")
# drug.takers 에 해당 약 저장
user = User.objects.get(id=user_id)
if drug not in user.durgslist.all():
user.durgslist.add(drug)
user.save()
else:
return Response(
data={"message": "이미 등록된 약입니다."}, status=status.HTTP_200_OK
)
return Response(
data={"message": "마이페이지에 저장되었습니다."}, status=status.HTTP_200_OK
)
except:
drug = Drug.objects.create(
name=drug_data["name"],
company=drug_data["company"],
drug_image=drug_data["drug_image"],
form=drug_data["form"],
ingredient=drug_data["ingredient"],
# user 애트리뷰트도 =request.user 로 저장
)
user = User.objects.get(id=user_id)
if drug not in user.durgslist.all():
user.durgslist.add(drug)
user.save()
return Response(
{"message": "마이페이지에 저장되었습니다."}, status=status.HTTP_201_CREATED
)
except:
return Response(
{"message": "등록되어 있지 않은 알약입니다. 정보를 직접 입력해주세요"},
status=status.HTTP_400_BAD_REQUEST,
)
우선 get 으로 약 모델을 가져온 다음 에러 - 해당 drug 가 존재하지 않음 - 가 나게 되면 create 를 한 뒤 user.durgslist 에 저장하는 식으로 처리했다. drugslist 인데 모델 설정해줄 때 오타가 나서 그냥 그대로 진행했다.
처음에는 serializers.SerializerMethodField() 을 이용해서 username 을 받고 그걸로 User.objects.get() > drugs_list = user.durgslist 를 한 뒤 for 을 써서 drug objects 를 가져오려 했는데 잘 되지 않았다. ManyToManyField 는 반복문을 돌리는 것이 불가능하다는 에러가 계속 떴다...
그래서 대체제로 StringRelatedField 를 써주기로 하였다. 우선 drugs/models.py 에서 Drug 모델에 def str(self) 함수를 추가해준다.
drugs/models.py
from django.db import models
# Create your models here.
class Drug(models.Model):
name = models.CharField(max_length=100)
company = models.CharField(max_length=100)
drug_image = models.ImageField(null=True, blank=True, upload_to="media/drugImg")
form = models.CharField(max_length=100)
ingredient = models.TextField(max_length=200)
def __str__(self): # 여기 추가
return self.name
본인의 string field 를 drug object 가 아닌 본인 name 으로 출력한다는 얘기이다. 이렇게 해주면 admin 에도 오브젝트가 name 으로 출력된다.
accounts/serializer.py
class ProfileSerializer(serializers.ModelSerializer):
durgslist = serializers.StringRelatedField(many=True)
class Meta:
model = User
fields = (
"nickname",
"username",
"email",
"profile_img",
"durgslist",
"created_at",
"updated_at",
)
이러면 이제 프론트에 데이터를 전달해줄 때도 약의 이름으로 전달해줄 수 있다.
로그인을 했을 때는 token 을 가져와 존재한다면 로그아웃, 프로필 이미지가 로그아웃 했을 때는 token 이 null 이니 로그인 회원가입 디폴트 이미지가 보이도록 네비게이션 바를 만들어뒀는데, 로그아웃을 했을 때는 LocalStorage 에 저장된 데이터가 모두 삭제되어 가져오는 과정 - LocalStorage.getItem() - 자체에서 에러가 나는 이슈가 있었다.
null 이든 None 이든 getItem() 함수 자체에서 나는 에러 같아서 그냥 try ~ catch 문으로 대신 처리해 주었다.
$(document).ready(function () {
try {
const accessToken = localStorage.getItem("access");
const storage = JSON.parse(localStorage.getItem("payload"));
const user_id = storage["user_id"];
let temp_html1 = ``;
console.log("accessToken" + accessToken);
if (accessToken) {
temp_html1 = `
<a class="logout" href="javascript:handleLogout()">로그아웃</a>
<a class="profile" href="mypage.html">
<img class="bar_profile" src="img/profile.jpg" id="my_profile_img" alt="profile" />
</a>
`;
}
$("#nav_bar").append(temp_html1);
console.log("append");
} catch (e) {
console.log("no account");
temp_html1 = `<a class="login" href="login.html">로그인</a
>
<a class="signup" href="signup.html">회원가입</a>
<a class="profile" href="mypage.html">
<img class="bar_profile" src="img/profile.jpg" id="my_profile_img"
alt="profile" />
</a>`;
}
$("#nav_bar").append(temp_html1);
console.log("append");
});
그리고 id 를 중복해서 쓰거나 잘못 쓴 이슈로 이미지가 한 쪽에만 올라가던가 아예 보이지 않는 문제가 있었다... id 를 정확히 쓰자!
프로필 이미지를 바꾸고 수정을 눌러도 수정이 되지 않는 이슈가 생겼다. 프론트에서 전달이 안 되는 줄 알고 form 도 바꿔보고 url 도 커스텀해가면서 프론트 쪽에서 애를 썼는데,백에서 request.FILE
을 찍어보고 프론트는 제대로 줬다는 것을 알게 되었다.
그리고 백을 열심히 뒤져본 결과, serializer 에 문제가 있었다는 것을 알게 되었다. django 템플릿을 썼을 때 이미지를 image.url 로 처리했기에 class MyPageSerializer(serializers.ModelSerializer) 에서 SerializerMethodField()을 통해 obj.user.profile_img.url 로 profile_img 를 return 해줬는데, 그게 문제가 되었다. 수정하기 전의 유저 이미지, 즉 defualt.png 를 받아왔던 것이다. 메소드 필드부터 깔끔하게 지워줬더니 제대로 작동되는 결과를 볼 수 있었다.
accounts/serializer.py
class ProfileSerializer(serializers.ModelSerializer):
durgslist = serializers.StringRelatedField(many=True)
class Meta:
model = User
fields = (
"nickname",
"username",
"email",
"profile_img",
"durgslist",
"created_at",
"updated_at",
)
처음으로 딥러닝 모델 api 를 사용하고 웹 크롤링을 직접 구현해봤는데, 생각보다 어렵고도 재밌었다. 직접 오류를 찾는 과정은 머리가 아프다 못해 화가 났지만 밤까지 같이 새준 의리있는 팀원분들이 계셔서 감사했다. 그리고 서로가 맡은 부분을 제대로 알고 계속 소통해주셔서 리팩토링할 때도 비교적 쉽게 할 수 있었다.
저번에도 느꼈지만 소통이 정말 중요했던 것 같다. 팀원들이랑 많이 소통하고, 안 되면 물어보고. 그 간단한 활동으로 지난 프로젝트에서는 경험할 수 없었던 값진 경험들을 많이 했던 것 같다. 배포까지 했으면 좋았겠지만 실패하면서 배운 바도 있으니 이번에는 제법 프로젝트 다운 프로젝트라고 할 수 있겠다 싶었다. 그리고 저번에 실패했던 프론트 - 백엔드 연결도 잘 한 것 같아 많이 뿌듯하다!
프로젝트를 마무리하면서 트러블 슈팅과 느낀점을 잘 정리해주셨네요👍