미니 프로젝트 결산 ( Sparta Travler )
어찌저찌 끝났다. 솔직히 어디서부터 손대서 뭘 해야할지 막막했는데 좋은 팀원분들과 함께해서 좋은 결과물도 나오고
협업이라는 과정도 순탄하게 잘 경험한 것 같다.
프로젝트 주제는 여행지, 맛집 등 장소를 리뷰하는 플렛폼 SpartaTraveler 입니다.
프로젝트는 22.8.1 - 22.8.4 로 진행되었고 첫 프로젝트의 과제의 난이도치고는 시간이 짧지 않았나싶다.
하지만 단기간에 성장하는 양은 앞도적이였던 것 같다.
이전 현재 상황 글을 보면 수준이 단순히 로그인, 회원가입을 그냥 만들어내는 수준이였는데,
막상 본격적으로 이렇게 작업을 시작하니 로그인 하나, 회원가입 하나에도 다양한 방법과 방식이 존재했다.
뭐 아무튼 잡설은 짧게하고 프로젝트를 분석하며 뜯어보려고 한다.
개발중요도와 개발 순서에 따라 하나씩 꺼내보겠습니다.
1. 서버 (Python 파일)
이번에 로그인을 구현하면서 아이디 저장 기능과 자동 로그인 기능을 추가했습니다.
아이디 저장은 쿠키에서 출력하도록 하였지만, 자동로그인 기능은 JWT발행을 통해 로그인과 별도로 발급했습니다.
2개의 토큰을 확인해야했기 때문에 유효성 검사가 이중으로 들어갔습니다.
1) JWT 토큰 방식 유효성 검사 로그인
token_receive = request.cookies.get('mytoken')
savetoken_receive = request.cookies.get('save_login')
# 자동 로그인 토큰 유효성검사
try:
payload2 = jwt.decode(savetoken_receive, SECRET_KEY, algorithms='HS256')
user_info = db.users.find_one({'userid': payload2['id']})
return render_template('index.html', id=user_info['userid'], name=user_info['name'])
# 토큰 만료 예외처리
except jwt.ExpiredSignatureError:
# 자동로그인 토큰이 없거나 예외처리 시 로그인 토큰 유효성 검사
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms='HS256')
user_info = db.users.find_one({'userid': payload['id']})
return render_template('index.html', id=user_info['userid'], name=user_info['name'])
# 토큰의 유효기간이 만료되었다는 에러문구
except jwt.ExpiredSignatureError:
return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
# 토큰이 유효하지 않다는 에러문구
except jwt.exceptions.DecodeError:
# return jsonify({'result': 'fail', 'msg': "로그인이 필요합니다!"})
return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))
# 토큰 헤더, 푸터, 토큰 유무 이상 예외처리
except jwt.exceptions.DecodeError:
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms='HS256')
user_info = db.users.find_one({'userid': payload['id']})
return render_template('index.html', id=user_info['userid'], name=user_info['name'])
# 토큰의 유효기간이 만료되었다는 에러문구
except jwt.ExpiredSignatureError:
return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
# 토큰이 유효하지 않다는 에러문구
except jwt.exceptions.DecodeError:
# return jsonify({'result': 'fail', 'msg': "로그인이 필요합니다!"})
return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))
첫 번째 try구문은 자동로그인 토큰을 우선 확인하고 만약 자동로그인 토큰이 없으면 로그인 토큰을 돌려 확인하는 방식을 사용했습니다. 처음에는 팀장님께서 자동로그인과 아이디저장을 다 서버에서 구현하시려고 고생하셨는데, 아이디 저장은 쿠키방식으로 html에서 처리하고 자동로그인만 토큰 하나 추가발행하여 검사했습니다.
유효성 검사 제작하면서 Exceptions 에러가 많이 나왔었는데 원인을 알고보니 밑에 함수랑 너무 가까워서....
파이썬 작업할 때 1줄 말고 2줄 차이나게 신경 써야겠다는 것을 배웠습니다.
JWT방식이 기존 만들었던 session방식보다 서버에 부담도 덜가고 보안도 서버에서 암호화를 통해 사용할 수 있어서
어느정도 개선된 세션방식이지 않나 더 좋은 방법이 있을거라 구상해 보도록 하겠습니다.
JWT방법 제작할 때 처음에 라이브러리 flask-jwt-os써서 매니저하는 함수로 제작했었는데 암호화하고, 이것 저것 검사하다보니 그 함수 쓰면 애러가 나더랍니다. 이렇게 만들때는 import jwp 쓰세요.
2) 회원가입 유효성 검사
reg = "^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$"
# 공백이 있는지 유효성검사
if id_receive == "" or password_receive == "" or password2_receive == "" or name_receive == "":
return jsonify({'result': 'fail', 'msg': '작성되지 않은 정보가 있습니다.'})
else:
# 비밀번호 유효성 검사
if bool(re.match(reg, password_receive)):
# 중복 아이디가 있는지 유효성 검사
if (db.users.find_one({'userid': id_receive})) is not None:
return jsonify({'result': 'fail', 'msg': '중복되는 아이디가 있습니다.'})
else:
if (db.users.find_one({'name': name_receive})) is not None:
return jsonify({'result': 'fail', 'msg': '중복되는 닉네임이 있습니다.'})
# 비밀번호는 잘못입력하면 계정을 못쓸 수 있기때문에 1,2를 통해 검사함 (동일한지 유효성검사)
else:
if password_receive != password2_receive:
return jsonify({'result': 'fail', 'msg': '비밀번호가 일치하지 않습니다.'})
else:
# pw 암호화 (hexdigest = 16진수로 암호화)
pw_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
# DB에 저장될 딕셔너리형 자료형으로 대입
doc = {
'userid': id_receive,
'password': pw_hash,
'name': name_receive
}
# 데이터베이스 저장
db.users.insert_one(doc)
return jsonify({'result': 'success', 'msg': '회원가입 완료'})
else:
return jsonify({'result': 'fail', 'msg': '비밀번호의 형식을 확인해주세요. 영문과 숫자 필수 포함, 특수문자(!@#$%^&*) 사용가능 8-20자'})
처음에는 공백 검사할 때 is None으로 검사를 했었는데 잘 되는가 싶다가 다음날 할 때는 안되고 이상한 것 같아서
공백확인을 == 방식으로 변경했습니다.
아이디, 닉네임, 비밀번호 다 유효성 검사가 있는데 너무 길어서 밑에있던 비밀번호에서 짤랐습니다.
reg를 통해서 정규식을 작성하면 정규식과 비교하여 해당하지않으면 에러를 출력하는 방식입니다.
중요한 것은 클라이언트에서 서버로 받아오는 것과 hashlib 을 통해서 비밀번호를 암호화 (인코딩)하는 과정입니다.
3) 파일 저장
# 파일을 받아와서 파일의 확장자명을 분리하는 값을 만드는 과정
file = request.files['file_give']
extension = file.filename.split('.')[-1]
# datetime함수를 통해 날짜 값을 받아옴 import datetime이냐 from datetime import datetime이냐에 따라 넣는 값이 달라짐
today = datetime.now()
mytime = today.strftime('%y-%m-%d-%H-%M-%S')
mydate = today.strftime('%y.%m.%d')
filename = f'file-{mytime}'
입력받은 파일을 확인하여 확장자랑 이름으로 구분하고, 서버에 등록된 이름이 겹치지않도록 현재 시간을 파일 이름으로 저장하도록 하였습니다. datetime 내장함수 사용하시면 됩니다.
# 서버에 파일 저장할 때 확장자명과 파일이름을 저장
# 여기서 받아왔을 떄는 이미지파일말고도 이미 파일 자체를 받은 상황이므로 html에서 지정해주는게 편할듯 or 조건문써서 실패랑 메세지 출력하게 해도 됨
if extension == "jpg" or extension == "gif" or extension == "png" or extension == "bmp":
save_to = f'static/img/{filename}.{extension}'
file.save(save_to)
파일 확장자는 이미지파일 일부로 지정하였고, f를 붙여서 파일 경로랑 이름설정하여 저장하도록 설정됩니다.
서버에 저장이 되어있어서 사용자가 서버로부터 파일을 받아서 볼 수 있습니다.
# 전역변수로 설정된 값은 문서번호를 만들기 위함
# 가능한 전역변수로 코딩을 안하는 것이 좋으나 문서번호의 고정값을 유지하기 위해 사용
num = 0
고민을 많이 했던게 데이터베이스에 글 저장을 하는데 문서번호를 어떻게 저장하면 좋을지 고민을 많이했습니다.
처음 제작 알고리즘은 문서갯수를 가져와서 +1 카운트해서 넣어주려고 했는데,
제작 계획에서 게시글 삭제가 기능으로 들어가 있어서 넘버가 꼬일 것 같아 사용하지않았습니다.
자동으로 할당되는 _id 순서와 연관 지으려다가 혹시나 싶어서 안했는데,
일단은 전역변수로 지정하니까 제대로 작동하네요.
4) 검색기능
# 검색어를 판단하여 데이터베이스에서 검색과 맞는 유효한 정보만 리스트로 보내줌
@app.route('/search', methods=['POST'])
def search_show():
find_name = request.form.get('find_name_give', False)
search_name = request.form.get('search_name_give', False)
print(find_name, search_name)
posts = list(db.posts.find({find_name: {'$regex': search_name}}))
return jsonify({'all_posts': dumps(posts)})
검색창에 입력한 값과 필터박스의 결과를 합쳐서 결과를 검색하면 나온 리스트를 보내주는 방법입니다.
상세페이지 만들 때도 검색하여 특정 한개만 보내주는 방식을 사용하려고 했는데,
jsonify에 리스트로 보내줘야 작동하고 한개의 데이터값을 찾아서 넣는건 시도해봤는데 안되더라구요.
2. 로그인
1) 자동 로그인 & 아이디 저장
window.onload = function() {
//저장 o
if ($.cookie('save_email')) {
$("#inputid").val($.cookie('save_email'))
$("#remember_ckb").prop("checked",true)
}
//자동 로그인 감별
if ($.cookie('save_login')) {
window.location.href("/index")
}
}
로그인 화면으로 왔을 때 자동로그인과 아이디 저장 체크 여부를 확인합니다.
만약 아이디 저장이 체크가 되어있으면 쿠키에서 id정보를 받아 넣어주고 체크박스를 유지하는 조건문이 첫번째,
두번째로, 자동로그인을 확인하여, 자동로그인 쿠키가 존재하면 로그인을 거치지않고 바로 메인화면으로 보내줍니다.
2) 쿠키
<!-- 쿠키를 사용하기 위한 javascript -->
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
if($("input:checkbox[name='save']:checked").val()){
// $.cookie('쿠키이름', 저장될 값, { expires: 저장기간, path: 쿠키가사용될경료(/일시 사이트 모든페이지), domain: 쿠키가 적용되는 도메인(기본은 생성된 도메인) secure: true사용시 https프로토콜에서만 사용가능 });
$.cookie('save_email', inputid, { expires: 365, path: '/login'});
//로그인에서만 쿠기를 사용하니 경로는 /login으로 설정
}
else{
$.removeCookie('save_email', {path: '/login'});
}
코드 일부만 가져왔습니다. 아이디 저장과 자동로그인 둘다 쿠키를 발급하는데 이메일은 쿠키로, 자동로그인은 jwt 입니다.
처음에 토큰발행하여 사용할 때, $.cookie는 빨간줄이 안생기는데 $.removeCookie는 빨간줄이 생겨서
알고보니까 js를 선언을 안해줘서 에러가 발생했습니다.
3) jinja2 템플릿
{% block login %}
{% endblock %}
상속 바디에 block으로 묶어줘서 해당부분만 작성하여 회원가입을 구성했습니다.
3. 메인화면
1) 리스트 출력
function listing() {
$.ajax({
type: "GET",
url: "/write2",
data: {},
success: function (response) {
let posts = response['all_posts']
for (let i = 0; i < posts.length; i++) {
let title = posts[i]['title']
let star = posts[i]['star']
let star_image = '⭐'.repeat(star)
let address = posts[i]['address']
let content = posts[i]['content']
let picture = posts[i]['file']
let date = posts[i]['date']
let num = posts[i]['num']
let tmep_html = `
<div class="column is-one-quarter">
<div class="card" value=${num} onclick="card_click(this)">
<div class="card-image">
<figure class="image is-4by3">
<img src="../static/img/${picture}" alt="Placeholder image">
</figure>
</div>
$('#cards-box').append(temp_html)
function card_click(obj) {
let url = "/detail?num_give=" + $(obj).attr("value")
window.location.href = url
}
카드는 더 긴데 잘랐습니다.
우선 서버에서 게시글 리스트를 받습니다. 별은 숫자로 받아와서 repeat 함수를 통해서 갯수만큼 출력합니다.
데이터 값을 다 넣은 카드를 만들어 #cards-box 라는 id를 가진 칸에 append를 통해 넣습니다.
게시글은 num이라는 번호를 가지고 있고 카드를 클릭하면 url에 문서번호를 받아와 넣어줍니다.
데이터 값입력은 ``을 통해서 내부에 ${데이터}로 받아옵니다.
3) 지도 API 숨기기, 보이기
function mapshow(button) {
var googlemap = document.getElementById('googleMap');
console.log(googlemap.style.display);
if (googlemap.style.display == 'block') {
googlemap.style.display = 'none';
$(button).text('지도 보기');
document.getElementById('cf').style.marginTop = '250px';
}
else {
googlemap.style.display = 'block';
$(button).text('지도 숨기기');
document.getElementById('cf').style.marginTop = '600px';
}
}
글 작성과 세부페이지에서 볼 수 있는 기능입니다. 두 페이지 모두 메인페이지에서 상속받기 때문에 넣었습니다.
버튼을 클릭하면 block과 none을 통해서 숨기고 꺼내고 할 수 있습니다.
4. 글 작성 페이지
1) 파일입력
function posting() {
let title = $('#inputtitle').val()
let content = $("#content").val()
let star = $(':radio[name="rating"]:checked').val()
let address = $('#address').val()
let file = $('#file')[0].files[0]
let form_data = new FormData()
if (file == null) {
alert("사진을 등록해주세요.")
} else if ($(':radio[name="rating"]:checked').length == 0) {
alert("별점을 등록해주세요.")
} else {
form_data.append("file_give", file)
form_data.append("title_give", title)
form_data.append("star_give", star)
form_data.append("address_give", address)
form_data.append("content_give", content)
$.ajax({
type: "POST",
url: "/write2",
data: form_data,
cache: false,
contentType: false,
processData: false,
success: function (response) {
if (response["result"] == 'success') {
alert(response["msg"])
window.location.href = '/'
} else {
//로그인 실패시 에러메세지 출력
alert(response["msg"])
}
}
});
}
파일을 입력할 때는 다른 데이터를 따로보내지 않고 form_data를 이용해서 값을 넣어주고 서버로 보내줍니다.
헷갈리던게 체크박스가 뒤집혀있어서 값이 조금 헷갈렸습니다.
파일도 처음엔 그냥 받도록 되어있었는데, 파일 입력시 확장자 검사해서 제어하는 것으로 바꼈습니다.
입력 안한 값이나, 유효성검사에 걸리는 내용이 있으면 입력을 못하게 막아놨었는데
개발때 데이터베이스 문구를 공유했더니 다른 개발자분이 만드셨던 내용으로
로컬에 열려있어서 막아둔게 뚫려가지고 올라갈 수 없는 값이 올라가더라구요. 데이터베이스 관리 조심하세요.
로컬환경과 구분해서 사용해야합니다.
2) 지도 API 출력
<script>
var cityHall;
function myMap() {
var marker_length = 0;
var marker;
var mapOptions = {
center: new google.maps.LatLng(37.566554, 126.978546),
zoom: 5
};
var map = new google.maps.Map(document.getElementById("googleMap"), mapOptions);
var geocoder = new google.maps.Geocoder();
document.getElementById('addressbutton').addEventListener('click', function () {
geocodeAddress(geocoder, map);
});
function geocodeAddress(geocoder, resultMap) {
var address = document.getElementById('address').value;
// 마커 지우기
if (marker_length > 0) {
marker.setMap(null);
marker_length == 0;
}
geocoder.geocode({'address': address}, function (result, status) {
if (status == 'OK') {
resultMap.setCenter(result[0].geometry.location);
resultMap.setZoom(5);
cityHall = result[0].geometry.location
marker = new google.maps.Marker({map: resultMap, position: cityHall});
marker_length = 1
} else {
alert('주소가 정확하지 않습니다.' + status);
}
});
}
}
</script>
지도를 넣어주는 스크립트 내용입니다. 지도를 검색해서 지역이 아니면 주소 에러가 발생하고,
제대로 된 주소를 넣으면 마커가 찍히는 내용입니다
5. 세부 페이지
1) 헤더 블럭 나누기
{% block post %}
<script>
$(document).ready(function () {
bsCustomFileInput.init()
show()
})
</script>
{% endblock %}
메인화면과 화면이 달라서 헤더를 따로 넣었습니다.
페이지에 들어갔을 때 위의 링크를 읽어와서 데이터베이스의 id값과 비교하여 해당하는 문서를 받아옵니다.
jinja를 헤더로 쓰고싶은데 스크립트 내용만 바꾸고 싶어서
스크립트 안쪽에다가 {% block %} 을 나눴습니다.
자바 스크립트 안에서는 동작을 안해서 jinja2는 html에서 동작하는걸 알고 스크립트 외부로 바꿔서 넣었습니다.
그래서 내부의 함수들을 미리 적어놨던걸 못써서 상세페이지로 옮겨서 필요한 함수를 따로 적어줬습니다.
주요 기능과 어려웠던 코드들 지금보니까 쉽게쓸 수도 있었는데 만들 때는 왜 그렇게 에러도 많이 내면서 고생했는지..
실수도 많고 실패도 있었지만 나름 보람있고 뿌듯한 결과물이 나온거 같아서 즐겁게 작업했던 것 같습니다.
Git 자료 공유 : https://github.com/Yoepee/til
GitHub - Yoepee/til: 이노베이션 프로젝트
이노베이션 프로젝트. Contribute to Yoepee/til development by creating an account on GitHub.
github.com
Youtube: https://www.youtube.com/watch?v=iRkpHzd3kvg