iPark Project
피드백 사항 반영
- 어제 하던 것들에 이어 피드백을 수정하고 있다.
- 어제 하던 것들을 완벽하지 않지만 조금씩 수정한 부분이 있다.
회원가입 에러 메세지 반영 부분 수정
- 어제 넘어왔던 에러 메세지는 django의 기본 validator에서 보내주던 에러 메세지였다. 따라서 그냥 빈 값을 백앤드로 보내면 django가
빈값 싫어 값 내놔
하면서 자동으로 key값을 붙여 메세지를 보내주던 것이었다.
- 따라서 내가 직접 키값을 작성하지 않아도
{"키값" : "에러 메세지"}
의 형태로 데이터를 보낸 것이다.
- 하지만 오늘 수정한 부분은 custom validator의 에러 메세지의 키값이며, 지금까지 나는
error
라는 키값으로 메세지들을 보내고 있었다.
- 이 부분을 메세지에 맞는 키값의 이름으로 변경해 주었다.
def validate(self, data):
correct_password = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$")
password_input = correct_password.match(data.get("password", ""))
if data.get("username"):
try:
if UserModel.objects.get(username=data.get("username")):
raise serializers.ValidationError(
detail={"username": "이미 존재하는 username입니다."}
)
except:
if not len(data.get("username", "")) >= 6:
raise serializers.ValidationError(
detail={"username": "username의 길이는 6자리 이상이어야 합니다."})
if password_input == None:
raise serializers.ValidationError(
detail={"password": "비밀번호는 8 자리 이상이며 최소 하나 이상의 영문자, 숫자, 특수문자가 필요합니다."})
return data
- 또한 javascript에 아래 부분을 추가하였다.
- 어제 작성했던 코드는
switch문의 모든 case마다
차일드 노드를 삭제하는 코드를 작성했었다. 그렇게 했더니 에러 메세지가 없는 경우에는 차례대로 마지막에 위치한 다른 노드들까지 전부 지워버렸기 때문이다.
- 각 case마다 작성하는 것은 반복이기 때문에 좋지 않아 일단 이 부분이라도 반복을 줄여보았다.
var error_node = document.querySelectorAll("#error")
error_node.forEach(
error =>
error.parentNode.removeChild(error)
)
- 다음과 같이 작동한다.
회원 정보 수정 코드 피드백 반영
- 내가 만든 회원정보 수정 페이지는 회원가입과 똑같은 폼을 사용하고 있다.
- 별 생각없이 비슷한 폼을 유지하는 것이 좋을 거 같아 만들게 되었고, 회원가입의 html과 js를 변경하게 되면서 이 부분에 대한 피드백도 같이 반영해 수정해 보았다.
- 이 페이지의 피드백은 다음과 같다.
- 사용자 인증을 하는데 아이디와 비밀번호를 제대로 쳐도 존재하지 않는 사용자라는 에러가 난다. (이거는 왜 그런지 모르겠다.)
- 로그인을 두 번 하는 거 같아 불편하다.
- 회원탈퇴를 할 때
정말 탈퇴하겠습니까?
라고 한 번 더 물어보는 것이 좋을 것 같다.
- 에러 메세지의 세분화
- 계정관리를 할 때 페이지를 변경하는 것이 아니라
팝업을 사용하는 것도 좋을 것 같다
는 피드백도 주셨었는데, 나는 페이지를 변경해 계정관리를 하는 곳이라는 것을 보여주고 싶었기 때문에 이 부분은 반영하지 않았다.
- 위에서 3번째와 4번째 부분을 일단 수정해 보았다.
- 먼저 계정 관리를 할 수 있는지
권한을 확인
하는 함수이다.
- 이메일을 직접 입력하는 것이 아닌
select
태그를 사용하고 있기 때문에 이메일의 앞 부분과 도메인 부분을 따로 받아왔다. (split 사용)
async function searchUser() {
const userData = {
username: document.getElementById("checkUsername").value,
password: document.getElementById("checkPassword").value
}
const response = await fetch(`${backendBaseUrl}/user/verification/`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-type": "application/json",
"Authorization": "Bearer " + localStorage.getItem("access")
},
body: JSON.stringify(userData)
})
verification_json = await response.json()
if (response.status == 200) {
const popup = document.getElementById("popup")
popup.style.visibility = "visible"
document.getElementById("accountUsername").value = verification_json.username
document.getElementById("accountFullname").value = verification_json.fullname
document.getElementById("accountEmail").value = verification_json.email.split("@")[0]
document.querySelector("#email-field select option").value = "@" + verification_json.email.split("@")[1]
document.querySelector("#email-field select option").innerHTML = "@" + verification_json.email.split("@")[1]
document.getElementById("accountPhone").value = verification_json.phone
document.getElementById("accountRegion").value = verification_json.region
} else {
alert(verification_json["message"])
const popup = document.getElementById("popup")
popup.style.visibility = "hidden"
}
}
- 계정 관리 페이지에서 수정한 정보를 백앤드로 보내는 함수이다.
- switch를 통해 각 상황에 맞는 에러 메세지를 보여줄 수 있도록 했다.
- 물론 회원가입과 다르게 이미 회원가입할 때 작성한 데이터가 이미 들어가 있어 많은 에러가 나지는 않을 것이라 생각한다.
async function changeAccount() {
var error_node = document.querySelectorAll("#error")
error_node.forEach(
error =>
error.parentNode.removeChild(error)
)
let changedData
if (document.getElementById("accountPassword").value) {
changedData = {
username: document.getElementById("accountUsername").value,
password: document.getElementById("accountPassword").value,
fullname: document.getElementById("accountFullname").value,
email: document.getElementById("accountEmail").value + document.querySelector("#email-field select option").value,
phone: document.getElementById("accountPhone").value,
region: document.getElementById("accountRegion").value
}
} else {
changedData = {
username: document.getElementById("accountUsername").value,
fullname: document.getElementById("accountFullname").value,
email: document.getElementById("accountEmail").value + document.querySelector("#email-field select option").value,
phone: document.getElementById("accountPhone").value,
region: document.getElementById("accountRegion").value
}
}
const response = await fetch(`${backendBaseUrl}/user/`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-type": "application/json",
"Authorization": "Bearer " + localStorage.getItem("access")
},
body: JSON.stringify(changedData)
})
account_response = await response.json()
if (response.status == 201) {
alert("회원정보 수정이 완료되었습니다.")
window.location.replace(`${frontendBaseUrl}/index.html`)
} else {
const key = Object.keys(account_response)
const error = Object.values(account_response)
for (let i = 0; i < key.length; i++) {
switch (key[i]) {
case "username":
const err_username = document.getElementById("username-field")
var new_span = document.createElement("span")
new_span.setAttribute("id", "error")
new_span.innerText = error[i]
err_username.appendChild(new_span)
break
case "email":
const err_email = document.getElementById("email-field")
var new_span = document.createElement("span")
new_span.setAttribute("id", "error")
new_span.innerText = error[i]
err_email.appendChild(new_span)
break
case "fullname":
const err_fullname = document.getElementById("fullname-field")
var new_span = document.createElement("span")
new_span.setAttribute("id", "error")
new_span.innerText = error[i]
err_fullname.appendChild(new_span)
break
case "password":
const err_password = document.getElementById("password-field")
var new_span = document.createElement("span")
new_span.setAttribute("id", "error")
new_span.innerText = error[i]
err_password.appendChild(new_span)
break
case "phone":
const err_phone = document.getElementById("phone-field")
var new_span = document.createElement("span")
new_span.setAttribute("id", "error")
new_span.innerText = error[i]
err_phone.appendChild(new_span)
break
}
}
}
}
회원탈퇴 시 탈퇴 여부를 물어보도록 수정
- 기존에는 회원탈퇴 버튼을 누르면 바로 회원탈퇴가 진행되었다.
- 왜 만들때는 알아차리지 못했는지 모르겠지만, 굉장히 이상한 방식이었다.
- 따라서 탈퇴 여부를 물을 수 있도록
confirm
을 사용해 보았다.
Window.confirm()
메서드는 확인과 취소 두 버튼
을 가진다.
- 확인과 취소 버튼을 통해 각각
true
와 false
를 인자값으로 받아 조건문에 있는 것을 실행해준다.
- 나의 경우 true일 때 회원탈퇴를 진행하도록 하고, false일 때 아무 값도 넣지 않아 취소하게 되면 아무런 일이 일어나지 않고 해당 페이지에 머무르도록 했다.
- 코드는 아래와 같다.
async function withdrawal() {
var delConfirm = confirm("정말 회원 탈퇴를 진행하시겠습니까?")
if (delConfirm) {
const response = await fetch(`${backendBaseUrl}/user/`, {
method: "DELETE",
headers: {
Accept: "application/json",
"Content-type": "application/json",
"Authorization": "Bearer " + localStorage.getItem("access")
}
})
withdrawal_json = await response.json()
if (response.status == 200) {
alert(withdrawal_json["message"])
localStorage.removeItem("payload")
localStorage.removeItem("access")
localStorage.removeItem("refresh")
window.location.replace(`${frontendBaseUrl}/index.html`)
} else {
alert(withdrawal_json["message"])
}
}
}
- 아래와 같이 작동한다.
계정 관리 페이지에 들어가면 아이디 보이도록 조치
- 기존에는 아이디와 비밀번호를 모두 입력해야 계정관리 페이지로 들어갈 수 있도록 로직을 작성했었다.
- 이에 대해 사용자 피드백으로
두 번 로그인하는 것 같아 불편하다
라는 피드백이 왔었고, 이를 반영해 보완해보았다.
- 기존의 틀은 유지하되, 내가 필요한 부분은
현재 로그인한 사용자의 아이디를 보여주는 것
이었다.
- 이를 위해 먼저
jwt
토큰을 custom
하였다.
user/jwt_claim_serializer.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class iParkTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token['id'] = user.id
token['username'] = user.username
return token
from rest_framework_simplejwt.views import TokenObtainPairView
from user.jwt_claim_serializer import iParkTokenObtainPairSerializer
class iParkTokenObtainPairView(TokenObtainPairView):
serializer_class = iParkTokenObtainPairSerializer
from django.urls import path
from user import views
from rest_framework_simplejwt.views import (
TokenRefreshView
)
urlpatterns = [
path("", views.UserView.as_view(), name="user_view"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh_pair"),
path('api/ipark/token/', views.iParkTokenObtainPairView.as_view(), name='ipark_token'),
path("kakao/", views.KakaoLoginView.as_view(), name="kakao"),
path("myid/", views.FindUserInfoView.as_view(), name="myid_view"),
path("alterpassword/", views.AlterPasswordView.as_view(), name="alter_password_view"),
path("verification/", views.UserVerifyView.as_view(), name="user_verification_view"),
]
- 프론트의 코드는 아래와 같다.
payload
에 담겨있는 username을 가져와 input 창에 넣어 자동으로 보이게 하였고, 해당 값을 username.value
를 통해 백앤드로 보내주어 계정관리를 할 수 있게 하였다.
const username = document.getElementById("checkUsername")
username.value = JSON.parse(localStorage.getItem("payload"))["username"]
async function searchUser() {
const userData = {
username: username.value,
password: document.getElementById("checkPassword").value
}
const response = await fetch(`${backendBaseUrl}/user/verification/`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-type": "application/json",
"Authorization": "Bearer " + localStorage.getItem("access")
},
body: JSON.stringify(userData)
})
verification_json = await response.json()
if (response.status == 200) {
const popup = document.getElementById("popup")
popup.style.visibility = "visible"
document.getElementById("accountUsername").value = verification_json.username
document.getElementById("accountFullname").value = verification_json.fullname
document.getElementById("accountEmail").value = verification_json.email.split("@")[0]
document.querySelector("#email-field select option").value = "@" + verification_json.email.split("@")[1]
document.querySelector("#email-field select option").innerHTML = "@" + verification_json.email.split("@")[1]
document.getElementById("accountPhone").value = verification_json.phone
document.getElementById("accountRegion").value = verification_json.region
} else {
alert(verification_json["message"])
const popup = document.getElementById("popup")
popup.style.visibility = "hidden"
}
}
- 다음과 같은 화면이다.
계정관리 페이지 권한 인증 실패 시 에러 메세지 수정
- 원래는 정보를 잘못 입력하면
존재하지 않는 사용자입니다.
를 메세지로 사용하고 있었다.
- 정보가 잘못 입력되면 당연히 존재하지 않는 사용자아닌가? 라고 생각하고 작성했지만, 피드백에 다시 로그인을 시도하셨다는 분이 계셔서 다른 사람들이 봤을 때
내 계정 자체가 없다고?
라고 생각할 수도 있다는 생각이 들었다.
- 어짜피 계정관리 페이지가 변경되어 사용자 아이디가 자동으로 들어가 있는 것도 있어 메세지를
비밀번호가 일치하지 않습니다.
로 변경해 오해를 줄일 수 있도록 하였다.
회원정보 수정 시 동일 비밀번호 사용 못하도록 조치
- 회원정보를 수정할 때 비밀번호는 바꿔도 되고, 바꾸지 않아도 된다.
- 처음에는 생각하지 않고 있었는데, 비밀번호에 대한 정규 표현식도 이 부분에 필요하고 동일한 비밀번호에 대한 처리도 필요하다는 생각이 이미 배포를 하고 들었다!
- 이 부분에 대한 피드백은 없었지만 운 좋게 생각이 들어 미래의 피드백을 방어한 것 같다.
- 코드는 아래와 같다. custom updater를 통해 사용자의 정보를 수정하기 때문에 custom updater를 아래와 같이 수정하였다.
def update(self, instance, validated_data):
for key, value in validated_data.items():
if key == "password":
user = UserModel.objects.get(Q(username=validated_data["username"]) & Q(email=validated_data["email"]))
if check_password(value, user.password):
raise serializers.ValidationError(
detail={"password": "현재 사용중인 비밀번호와 동일한 비밀번호는 입력할 수 없습니다."})
else:
instance.set_password(value)
continue
setattr(instance, key, value)
instance.save()
return instance
- 작동 화면은 아래와 같다.