10-1 : 도서관 대여 서비스 (개인 프로젝트)
[엘리스 AI 트랙] 10주차 - 1
- comment 테이블 추가
- 책 상세 페이지 구현
- 상세 페이지에서 댓글 남기기 기능 구현
- 댓글 내용과 평점 null인 경우 댓글 등록 금지 기능 구현
- 메인 페이지에서 평균 평점 보여주는 기능 구현
- 이메일 중복 체크, 유효한 이메일 형식 체크
- 이름 유효한 형식 체크
- 비밀번호 유효한 형식 체크
- 현재 대여중인 책 중복으로 빌리지 못하게 하는 기능 추가
- 대여 기록 페이지 구현
TIL
1. comment 테이블 추가
mysql> create table `comment_tb`(
-> _id int primary key auto_increment not null,
-> user_id int not null,
-> book_id int not null,
-> comment text,
-> star_rating int not null,
-> created_at date,
-> foreign key(user_id) references user_tb(_id) on update cascade,
-> foreign key(book_id) references books_tb(_id) on update cascade);
-
상세 페이지에서 댓글을 남기면 기록할 comment 테이블을 추가했다. 내용 없이 별점만 남기는 경우가 많아서 별점만 not null로 만들었는데, 제약 조건을 다시 보니 댓글 내용도 null 이 아니어야 해서 alter로 수정했다.
(참고 : https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&blogId=siiyoo&logNo=70133007298)mysql> alter table comment_tb modify comment text not null;
- 제약 조건에 댓글을 보여줄 때 최신순으로 보여주는 기능이 있어서, date보다는 datetime으로 저장하는 게 나을 것 같아서 수정했다.
mysql> alter table comment_tb modify created_at datetime;
- 같은 내용 python sqlalchemy 테이블이다. 이 코드를 만들기 전에 자꾸 오류가 났었다.
db.Datetime
이 없다고 오류가 나서 구글링을 또 한참 열심히 했다. 결론은db.DateTime
이다. 대문자 한 끝 차이였다. 조심하자. (참고 : https://stackoverflow.com/questions/62262102/attributeerror-sqlalchemy-object-has-no-attribute-datetime)
class Comment(db.Model): __tablename__='comment_tb' # 1. 작성자 2. 책id 3. 내용 4. 별점 5. 작성일 _id = db.Column(db.Integer, primary_key=True, nullable=False, autoincrement=True) user_id = db.Column(db.Integer, db.ForeignKey('user_tb._id'), nullable=False) book_id = db.Column(db.Integer, db.ForeignKey('books_tb._id'), nullable=False) comment = db.Column(db.Text, nullable=False) star_rating = db.Column(db.Integer, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) def __init__(self, user_id, book_id, comment, star_rating): self.user_id = user_id self.book_id = book_id self.comment = comment self.star_rating = star_rating
- mysql에서 테이블 전체 데이터 삭제하는 방법
delete from mytable;
2. 책 상세 페이지 구현 & 3. 상세 페이지에서 댓글 남기기 기능 구현
-
책 이름 누르면 상세 페이지로 가게 url_for 이용
→ 여기서 url을 어떤 식으로 넘겨줄지 많은 고민과 에러와 싸운 끝에 다음처럼 구현.
(참고 : https://flask.palletsprojects.com/en/2.0.x/api/#flask.url_for)<h5 class="book-name"><a href="{{ url_for('.bookInfo', book_id = book._id) }}">{{ book.book_name }}</a></h5>
- info.html은 bootstrap에서 테이블 형식, form 태그 형식 등을 가져와서 구현했다.
(참고 : https://getbootstrap.com/docs/5.1/forms/layout/) - 백엔드 코드는 다음과 같다. 라우팅을 restful하게 작성해보았다.
(참고 : https://velog.io/@wody/Flask-Tutorial) - 요청이 get일 땐 info.html에서 해당 책의 모든 정보와 댓글들을 불러온다.
- 요청이 post일 땐 댓글 추가하면서 해당 책의 평균 평점도 계산해서 db에 반영한다.
- 평균 평점은 round 함수를 통해 반올림해주었다.
- 평균 평점을 구하기 위해 sqlalchemy에서 avg 함수를 어떻게 쓰는지 알아보았다.
from sqlalchemy import func
를 추가한다.ratings = db.session.query(db.func.avg(Comment.star_rating).label("rating_avg")).filter(Comment.book_id==book_id).first()
처럼 db.func.avg 형태로 쓸 수 있다. label을 추가하면 sql에서 as로 alias 설정(별명지어주는 기능)과 같다.- avg, min, max 등 사용법 참고 : https://www.py4u.net/discuss/149852
- sqlalchemy 이용법 참고 : https://lowelllll.github.io/til/2019/04/19/TIL-flask-sqlalchemy-orm/
- sqlalchemy에서 alias 설정 방법 : https://stackoverflow.com/questions/9187530/using-alias-for-select-as-in-sqlalchemy
@board.route("/info/<int:book_id>", methods=["GET", "POST"]) def bookInfo(book_id): book = Books.query.filter(Books._id==book_id).first() if request.method == "GET": # 책 정보 모두, 댓글 정보 모두 comments = Comment.query.filter(Comment.book_id==book_id).order_by(Comment.created_at.desc()).all() return render_template("info.html", book=book, comments=comments) else: # 댓글 추가 -> comment 테이블에 값 추가 commenter = request.form['commenter'] book_id = request.form['book_id'] comment = request.form['comment'] star_rating = request.form['star_rating'] c = Comment(commenter, book_id, comment, star_rating) db.session.add(c) db.session.commit() ratings = db.session.query(db.func.avg(Comment.star_rating).label("rating_avg")).filter(Comment.book_id==book_id).first() book.rating_avg = round(ratings.rating_avg) db.session.commit() return jsonify({"result": "success"})
- info.html은 bootstrap에서 테이블 형식, form 태그 형식 등을 가져와서 구현했다.
4. 댓글 내용과 평점 null인 경우 댓글 등록 금지 기능 구현
-
댓글 등록하기 버튼을 누르면 실행되는 javascript 함수에서 아래처럼 검사한다.
let comment = $("#comment").val() let star_rating = $("#star-rating").val() if (comment == '' || star_rating == '') { alert("댓글과 평점은 필수 항목입니다."); return; }
5. 메인 페이지에서 평균 평점 보여주는 기능 구현
-
본문 사이에 아래와 같이 추가해서 간단하게 구현했다. python에서 문자열 곱하기 기능을 이용했다.
<p class="stock">{{"★"*book.rating_avg}}</p>
6. 이메일 중복 체크, 유효한 이메일 형식 체크
-
유효한 이메일 형식은 구글링으로 정규표현식을 이용해서 처리했다.
- 유효한 이메일 정규표현식 참고 : https://www.w3resource.com/javascript/form/email-validation.php
- 정규 표현식 참고 : https://heropy.blog/2018/10/28/regexp/
-Uncaught TypeError: Cannot read properties of undefined (reading 'match')
에러 해결 : 위의 정규표현식 참고 페이지에서 복사해서 사용하니 이런 에러가 나왔다. 이는 if문 내부에input.value.match(mailformat)
으로 설정되어 있던 코드에서 value를 지워주니 해결되었다.function validateEmail(inputText) { let mailformat = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/; if(inputText.match(mailformat)) { return true; } else { alert("유효하지 않은 이메일입니다."); return false; } }
- 이메일 중복 체크는 백엔드 코드에서 다음처럼 이미 해당 email을 사용하는 user가 있는지 검사한다. 이미 있으면 duplicate라는 결과 값을 넘겨주었다.
user = User.query.filter(User.email == user_email).first() if user is not None: return jsonify({"result": "duplicate"})
- 받은 결과로 알림을 띄워준다. (ajax 내부 코드)
success: function (res) { if (res['result'] == 'success') { alert("회원가입 성공!"); window.location.href = '/' } else if (res['result'] == 'duplicate'){ alert("중복된 아이디입니다."); window.location.reload() } }
7. 이름 유효한 형식 체크
- 구글링으로 한글 또는 영문인지 검사하는 정규표현식을 이용했다.
- 참고 : https://codingbroker.tistory.com/119function validateName(inputText){ const regex = /^[ㄱ-ㅎ|가-힣|a-z|A-Z|]+$/; if(inputText.match(regex)){ return true; } else{ alert("이름은 영문 또는 한글만 입력 가능합니다."); return false; } }
8. 비밀번호 유효한 형식 체크
- 개인정보 보호조치 기준에 맞추어 영문, 숫자, 특수문자 3종류 이상을 조합하여 최소 8자리 이상의 길이인지 확인하는 정규표현식을 이용했다. 영문은 대소문자 구분 없이 포함하게 했다. 아래 링크들을 참고하여 구현하였다.
- 유효한 비밀번호 정규표현식 참고 : https://velog.io/@broccoliindb/password-validation-javascript
- 참고 2 : https://stackoverflow.com/questions/12090077/javascript-regular-expression-password-validation-having-special-characters```jsx function validatePW(inputText){ var strongRegex = new RegExp("^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})"); if(inputText.match(strongRegex)){ return true; } else{ alert("영문, 숫자, 특수문자를 모두 포함하여 8자리 이상의 비밀번호를 입력하세요."); return false; } } ```
9. 현재 대여중인 책 중복으로 빌리지 못하게 하는 기능 추가
-
백엔드 코드 : 현재 대여중인 책은 아직 반납일이 기록되지 않아 null 값이다. 따라서 반납일이 아직 설정되지 않은 같은 책이라면 중복이라고 결과를 전달한다.
TypeError: 'BaseQuery' object is not callable
에러가 나서 Querying with function on Flask-SQLAlchemy model gives BaseQuery object is not callable error 해당 링크를 참고하여 해결했다.
same_book = db.session.query(Rent).filter(Rent.book_id==book_id, Rent.user_id==user_id, Rent.return_date==None).first() if same_book is not None: return jsonify({"result": "duplicated"})
- 프론트 코드에서 ajax의 success 결과를 받아 다음 코드를 넣어주었다.
else if (result == "duplicated") { alert("이미 대여한 책입니다.") }
10. 대여 기록 페이지 구현
-
html 코드는 거의 return 페이지와 유사하다.
- 백엔드 코드도 return 부분과 비슷한데 다른 점 한가지는 반납일 컬럼 값이 null 값이 아니어야 반납된 책이다. 따라서 이미 반납된 책만 불러와야 한다. sqlalchemy에서 다음처럼 null값 검사를 할 수 있다. filter 내부에
Rent.return_date.isnot(None)
형식으로 조건을 줄 수 있다.Rent.return_date!=None
은 에러가 발생한다.
(참고 : https://stackoverflow.com/questions/21784851/sqlalchemy-is-not-null-select)
records = db.session.query(Books.img_path, Books.book_name, Books._id, Books.rating_avg, Rent.rent_date, Rent.return_date).filter(Books._id==Rent.book_id, Rent.user_id==g.user._id, Rent.return_date.isnot(None)).all()
- 대여 기록 페이지 결과 화면
- 백엔드 코드도 return 부분과 비슷한데 다른 점 한가지는 반납일 컬럼 값이 null 값이 아니어야 반납된 책이다. 따라서 이미 반납된 책만 불러와야 한다. sqlalchemy에서 다음처럼 null값 검사를 할 수 있다. filter 내부에
Author And Source
이 문제에 관하여(10-1 : 도서관 대여 서비스 (개인 프로젝트)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sue/9-6-도서관-대여-서비스-개인-프로젝트저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)