[해설 1] 프로젝트 해설: 전체 코드 기능 설명 1

오레오·2023년 12월 6일
0

오픈SW플랫폼

목록 보기
6/8

1. 개발환경 소개

  • 프론트엔드 : HTML, CSS, JavaScript
  • 백엔드 : flask
  • 데이터베이스 : firebase
  • 버전관리 : Git, GitHub

2. 백엔드 담당 파트

💡 1 ~ 6 [해설1] : 백_양지원
상품 등록, 등록한 상품 조회, 전체 상품 리스트 조회, 상품 상세보기 화면, 좋아요 및 다운로드 횟수 카운팅 기능, 구매 완료 화면 연결

💡 7 ~ 8 [해설2] : 백_황혜진
리뷰 작성, 등록한 리뷰 조회, 상품별 리뷰 조회 화면, 리뷰 상세보기 화면, 회원별 마이페이지 구현

💡 9 ~ 12 [해설3] : 백_윤소민
회원가입, 아이디 중복확인, 닉네임 중복확인, 로그인 및 로그아웃, 상품 구매시 포인트, 랭킹포인트 증감 기능, 사용자 포인트별 랭킹 정렬 기능

3. 기능과 코드

1. 상품 등록 화면 접근 시

[app.py]

@application.route("/1~4/item_reg")
def view_reg_items():
	if 'id' not in session or not session['id']:
		flash('상품을 등록하려면 로그인을 해주세요.')
		return redirect(url_for('login'))
	else:
		return render_template("1~4/item_reg.html")
  • 비로그인 상태의 경우, ‘상품을 등록하려면 로그인을 해주세요’ flash 창 팝업 후 로그인 화면으로 이동
  • 로그인 상태의 경우, 정상적으로 상품 등록 화면으로 연결

2. 상품 등록 화면에서 작성한 값 DB에 저장 & 상품 등록 후 상품 세부 보기 화면으로 연결

[item_reg.html]

<div id="regitem-container"> 
<form id="regitem" action="/submit_item_post" method="post" enctype="multipart/form-data">
     … 입력 내용 …  </form>
  • html에서 사용자가 입력한 내용들이 모두 ‘/submit_item_post’라는 링크로 넘어감. 해당 링크로 넘어갔을 때의 동작은 아래 app.py의 함수에서 담당함.

[app.py]

@application.route("/submit_item_post", methods=['POST'])

def reg_item_submit_post():
	if 'id' not in session or not session['id']:
		flash('상품을 등록하려면 로그인을 해주세요.')
		return redirect(url_for('login'))
	else:
		item_file=request.files['item_upload']
		item_file.save("static/items/{}".format(item_file.filename))
		photo_file=request.files.getlist("photo_upload[]")
		data=request.form
		writer = session['id']
		for f in photo_file:
			f.save('static/photos/' + f.filename)
		DB.insert_item(data['item_name'], data, item_file.filename, [f.filename for f in photo_file], session['id'])
	return render_template("1~4/item_detail.html", data=data, item_path="static/items/{}".format(item_file.filename), photo_paths=["static/photos/{}".format(f.filename) for f in photo_file])
  • 사용자가 등록한 상품(item) 저장: 사용자가 입력한 정보들을 넘겨받은 지점인 ‘request’로부터 ‘item_upload’라는 name을 찾아 item_file 변수에 저장한다. 그 item_file을 static/items/ 경로에 저장한다.
  • 사용자가 등록한 사진(photo) 저장: 사용자가 입력한 정보들을 넘겨받은 지점인 ‘request’로부터 ‘photo_upload’라는 name의 배열을 찾아 pthoto_file 변수에 저장한다. 그 photo_file 배열을 차례로 순회하며 static/photos/ 경로에 하나씩 저장한다.
  • 사용자가 입력한 모든 정보 저장: 사용자가 입력한 정보들을 넘겨받은 지점인 ‘request’로부터 저장한다.
  • insert_item()함수를 이용하여, 사용자가 등록한 상품의 이름, 등록한 모든 정보, 등록한 상품, 등록한 사진, 그리고 현재 접속 중인 id (상품을 등록한 유저)를 firebase에 저장한다.
  • 데이터베이스에 상품의 값을 모두 저장한 후, 정상 등록된 상품의 세부 정보를 보여주는 상품 세부화면으로 render_template함. 이때 html 파일에서 사용할 정보인 data, item_path, photo_path를 함께 전달한다.

[database.py]

#상품 정보 등록하기
    def insert_item(self, item_name, data, item_path, photo_path, user_id):
        item_info = {
            "writer": user_id,
            "item_name": data['item_name'],
            "item_type": data['item_type'],
            "price": data['price'],
            "course_type": data.get('course_type'),
            "faculty": data.get('faculty'),
            "major": data['major'],
            "course_number": data['course_number'],
            "professor": data['professor'],
            "description": data['description'],
            "tag": data['tag'],
            "item_path": item_path,
            "photo_path": photo_path,
            "download_count": 0
        }
        user_and_item = user_id + '_' + data['item_name']
        self.db.child("item").child(user_and_item).set(item_info)
        print(data, item_path)
        for path in photo_path:
            print("사진 경로:", path)
        return True
  • 사용자로부터 등록받은 정보를 다음과 같이 저장한다.
  • 등록한 정보들은 ‘item’ child 아래에 저장되며, 각 상품을에 대한 저장 항목의 이름은 ‘등록한 유저의 아이디_상품의 이름’으로 설정하여 서로 다른 유저가 같은 이름으로 상품을 등록하였을 때 database에 값이 덮어씌워지는 문제를 방지하였다.
  • 각 상품에 대한 다운로드 횟수를 측정하기 위해 사용자가 상품 등록 시 입력한 값 외에 ‘download_count’라는 key를 추가하였다.
  • 위 함수의 결과로 데이터베이스에 저장되는 상품의 정보는 다음과 같다.

3. 상품 전체보기 화면의 페이지네이션 구현

[app.py]

#### 맨 처음 화면이 이 view_items()함수로 옴.
@application.route("/1~4/view_item")
def view_items():
    page = request.args.get("page", 0, type=int)
    per_page=5 # item count to display per page
    per_row=1 # item count to display per row
    row_count=int(per_page/per_row)
    start_idx=per_page*page
    end_idx=per_page*(page+1)

    data = DB.get_items()
    item_counts = len(data)
    data = dict(list(data.items())[start_idx:end_idx])
    tot_count = len(data)

    for i in range(row_count): #last row
        if (i == row_count-1) and (tot_count%per_row != 0):
            locals()['data_{}'.format(i)] = dict(list(data.items())[i*per_row:])
        else: 
            locals()['data_{}'.format(i)] = dict(list(data.items())[i*per_row:(i+1)*per_row])
    return render_template("1~4/view_item.html", datas=data.items(), row1=locals()['data_0'].items(), row2=locals()['data_1'].items(), limit=per_page, page=page, page_count=int((item_counts/per_page) +1), total = item_counts)
  • View_items() 함수는 페이지 매개변수 ‘page’를 받아와 해당 페이지에 해당하는 아이템 목록을 가져와 템플릿으로 전달한다.
  • 페이지당 5개의 아이템을 표시하며, 각 행에 1개의 아이템이 표시된다. 페이지 수와 총 아이템 개수도 함께 전달된다.

4. 상품 전체보기 리스트에서 특정 상품 클릭 시 상품 세부보기 화면으로 연결

[view_item.html]

<div class="item-card" onclick="location.href='/1~4/view_item_detail/{{key}}/';" style="cursor:pointer;">
                            … 추가 내용 …                     </div>
  • 등록된 상품 전체를 리스트로 보여주는 html에서 특정 상품에 대한 item card를 클릭 시, ‘/1~4/view_item_detail/{{key}}/’ 로 연결된다.
  • 해당 연결 지점에 대한 py의 함수를 아래에 구현하였다.

[app.py]

#전체 리스트에서 상품 클릭 시 세부정보 볼 수 있음
@application.route("/1~4/view_item_detail/<item_name>/")
def view_item_detail(item_name):
    data=DB.get_item_byname(str(item_name))
    return render_template("1~4/detail.html", item_name=item_name, data=data)
  • 위 html 파일에서의 key 값은 사용자가 클릭한 item card에 대한 특정 상품이므로, 그 클릭된 상품 이름에 따라서 동적으로 라우팅된다.
  • get_item_byname 함수를 이용해, 사용자가 클릭한 상품의 이름을 key로 하여 DB에서 해당 아이템의 정보를 찾아 가지고 온다.
  • 찾은 정보 data와 item_name을 갖고, 상품의 세부 정보를 확인할 수 있는 상품 세부보기 화면 (1~4/detail.html)으로 render_template 한다.

[database.py]

#상품 이름으로 상품 정보 가져오기
    def get_item_byname(self, name):
        items = self.db.child("item").get()
        target_value=""
        for res in items.each():
            key_value = res.key()
            if key_value == name:
                target_value=res.val()
        return target_value
  • Firebase의 child("item").get() 메서드를 사용하여 "item" 경로의 데이터를 가져온 후, 반복문을 통해 각 상품을 확인하여 입력받은 이름과 일치하는 상품을 찾는다. 일치하는 상품이 발견되면 해당 상품의 정보를 변수에 저장하고 반환한다. 일치하는 상품이 없으면 빈 문자열을 반환한다.

5. 상품 세부

화면에서 좋아요 기능 구현

[Detail.html]

<script>
        function showHeart() {
        $.ajax({
            type: 'GET',
            url: '/show_heart/{{item_name}}/',
            data: {},
            success: function (response){
                let my_heart = response['my_heart'];
                if (my_heart['interested'] == 'Y')
                {
                    $("#heart").css("color","red");
                    $("#heart").attr("onclick","unlike()");
                }
                else
                {
                    $("#heart").css("color","grey");
                    $("#heart").attr("onclick","like()");
                }
                //alert("showheart!")
                }
            }); 
        } 

        function like() {
            $.ajax({
                type: 'POST',
                url: '/like/{{item_name}}/',
                data: {
                    interested : "Y"
                },
                success: function (response) {
                    alert(response['msg']);
                    window.location.reload()
                }
            });
        }
        
        function unlike() {
            $.ajax({
                type: 'POST',
                url: '/unlike/{{item_name}}/',
                data: {
                    interested : "N"
                },
                success: function (response) {
                    alert(response['msg']);
                    window.location.reload()
                }
                });
            }
            
        $(document).ready(function () {
            showHeart();
        });

    </script>
  • 상품의 세부 정보를 확인할 수 있는 상세페이지를 구현한 html에서, 하트 아이콘을 클릭할 시 동작하는 javascript 코드를 확인할 수 있다.
  • ShowHeart(): 해당 상품에 대해 현 사용자가 관심을 눌렀는지, 누르지 않았었는지 확인하기 위해 서버에 ajax get 요청을 보내는 함수이다. 이때 사용자가 이전에 누른 기록이 있다는 응답을 받으면 현재 클릭에 대한 결과를 unlike() 함수로 연결하고, 누른 기록이 없다는 응답을 받으면 현재 클릭에 대한 결과를 like() 함수로 연결한다.
  • Like(): 해당 상품에 대해 현 사용자가 하트를 클릭하는 경우 서버에 ajax post 요청을 보내고 응답을 받으면 alert 메시지를 띄운 후 페이지를 다시 로드한다.
  • Unlike(): 해당 상품에 대해 이미 하트를 눌렀던 사용자가 하트를 재클릭하여 관심을 취소하는 경우, 서버에 ajax post 요청을 보내고 응답을 받으면 alert 메시지를 띄운 후 페이지를 다시 로드한다.

[app.py]

#좋아요 관련 기능
@application.route('/show_heart/<item_name>/', methods=['GET'])
def show_heart(item_name):
    if 'id' not in session or not session['id']:
        flash('상품을 찜하려면 로그인을 해주세요.')
        return redirect(url_for('login'))
    else:
        my_heart = DB.get_heart_byname(session['id'],item_name)
        return jsonify({'my_heart': my_heart})
  • 비로그인 상태에서 하트를 누르는 경우, ‘상품을 찜하려면 로그인을 해주세요’라는 flash 알림을 띄운 후 login 화면으로 바로 redirect한다.
  • 로그인 상태에서 하트를 누르는 경우, DB.get_heart_byname 함수를 통해 사용자의 id와 상품 이름을 활용하여 해당 사용자가 이 상품에 대해 좋아요를 눌렀었는지 여부를 조회한다.
@application.route('/like/<item_name>/', methods=['POST'])
def like(item_name):
    if 'id' not in session or not session['id']:
        flash('상품을 찜하려면 로그인을 해주세요.')
        return redirect(url_for('login'))
    else:
        my_heart = DB.update_heart(session['id'],'Y',item_name)
        return jsonify({'msg': '좋아요 완료!'})
@application.route('/unlike/<item_name>/', methods=['POST'])
def unlike(item_name):
    if 'id' not in session or not session['id']:
        flash('상품을 찜하려면 로그인을 해주세요.')
        return redirect(url_for('login'))
    else:
        my_heart = DB.update_heart(session['id'],'N',item_name)
        return jsonify({'msg': '안좋아요 완료!'})
  • update.heart() 함수를 이용하여 데이터베이스에 해당 상품에 대한 좋아요 결과를 저장한다. 정상적으로 등록된 DB의 모습은 다음과 같다.

[database.py]

#heart 정보 가져오기
    def get_heart_byname(self, uid, name):
        hearts = self.db.child("heart").child(uid).get()
        target_value=""
        if hearts.val() == None:
            return target_value
        
        for res in hearts.each():
            key_value = res.key()
        
            if key_value == name:
                target_value=res.val()
        return target_value
  • 특정 사용자(‘uid’)의 하트 정보를 특정 상품(‘name’)에 대해 조회한다. 데이터베이스에서 해당 사용자의 하트 정보를 가져와서 반복문을 통해 특정 상품과 일치하는 정보를 찾는다.
#heart 값 변경하기    
    def update_heart(self, user_id, isHeart, item):
        heart_info ={
            "interested": isHeart
        }
        self.db.child("heart").child(user_id).child(item).set(heart_info)
  • 사용자 ID, 상품에 대한 하트 여부 (‘isHeart’), 상품 이름(item)을 인자로 받아서 업데이트할 정보를 딕셔너리 형태로 만든다. 해당 정보를 Firebase의 “heart” child에 업데이트한다.

6. 상품 세부 화면에서 구매 버튼 클릭 후 구매 완료 화면으로 연결

[Detail.html]

<div>
<button onclick="location.href='/1~4/order_item/{{item_name}}/';" title="to-payment" id="order-btn" class="button btnFade btnDefaultblue">바로 구매하기</button>
  • 상품 세부 화면에서 ‘바로 구매하기’ 버튼 클릭 시, ‘/1~4/order_item/{{item_name}}/’이라는 경로로 라우팅됨.

[app.py]

#구매하기 버튼 누르면

#구매하기 버튼 누르면
@application.route("/1~4/order_item/<item_name>/")
def view_order_confirmation(item_name):

    point=DB.get_price(str(item_name))
    seller=DB.get_seller(str(item_name))

    if 'id' not in session or not session['id']:
        flash('구매하시려면 로그인을 해주세요.')
        return redirect(url_for('login'))
    else:

        download_count = DB.increase_download_count(item_name) #다운로드 횟수 증가

        DB.update_point(session['id'], point) #구매자 포인트 감소
        DB.update_ranking_point(session['id'], point) #구매자 랭킹 포인트 증가
        DB.update_point_2(seller,point) #판매자 포인트 증가
        DB.update_ranking_point(seller,point) #판매자 랭킹 포인트 증가
        
        DB.insert_purchase_history(item_name, session['id'])
        seller_email = DB.get_seller_email(item_name)

        flash('포인트가 차감되었습니다')

        data=DB.get_item_byname(str(item_name))
        session['user_point'] = DB.get_user_point(session['id'])

    return render_template("1~4/order_item.html", data=data, item_name=item_name, seller_email=seller_email, download_count=download_count)
  • insert_purchase_history() 함수를 이용하여, 각 사용자별 구매한 상품의 리스트가 업데이트될 수 있도록 한다.
  • increase_download_count() 함수를 이용하여, 구매 버튼 클릭 시마다 해당 구매된 상품의 다운로드 횟수가 하나씩 증가될 수 있도록 한다.
  • get_seller_email() 함수를 이용하여, 구매한 상품의 이름을 key로 하여 구매된 상품의 판매자의 이메일 정보를 DB로부터 찾아올 수 있도록 한다.
  • get_item_byname() 함수를 이용하여, 구매한 상품의 이름을 key로 하여 구매된 상품의 모든 정보를 data 라는 변수에 담아온다.
  • 위 DB 함수들로 얻은 정보를 포함하여 ‘1~4/order_item.html’로 render_template한다.

[database.py]

def insert_item(self, item_name, data, item_path, photo_path, user_id):
        item_info = {
           … 추가 내용들 …
            "download_count": 0
        }
  • 각 상품에 대한 다운로드 횟수를 카운트하기 위해, 상품 최초 등록 시 각 상품에 대한 database에 “download_count”라는 key를 추가하였다. 최초 등록 시이므로 초기값은 0으로 설정하였다.
#사용자별 구매내역 저장하기
def insert_purchase_history(self, item_name, user_id):

      timestamp = int(time.time())

      purchase_info = {
          "item_name": item_name,
          "timestamp": timestamp
      }
      self.db.child("user_purchase_history").child(user_id).set(purchase_info)
        return True
  • Database에 “user_purchase_history”라는 child를 만들어, 그 아래 각 user_id별로 purchase_info를 key로 하여 저장한다. Purchase_info에는 구매한 상품의 이름과, 구매한 시간이 저장된다.
#구매 버튼 누를 때마다 download 횟수 하나씩 늘려 저장
    def increase_download_count(self, item_name):
        current_count = self.db.child("item").child(item_name).child("download_count").get().val()

        new_count = current_count + 1
        self.db.child("item").child(item_name).update({"download_count": new_count})
      return new_count
  • 현재 상품의 download_count 값을 db로부터 검색하여 가져오고, 그 값을 1 증가하여 유에 저장된 값을 업데이트한다.
#판매자 이메일 가져오기
    def get_seller_email(self, item_name):
        writer = self.db.child("item").child(item_name).get().val().get('writer')
        if writer:
            user_data = self.db.child("user").child(writer).get().val()
            if user_data:
                email = user_data.get('email')
                return email        return None
  • Firebase의 ‘item’ child에서 ‘item_name’에 해당하는 데이터를 찾아, 그 안의 writer 값을 가져온다.
  • 만약 writer 값이 존재한다면, firebase의 ‘user’ child에서 그 writer 값과 일치하는 데이터를 찾는다. 일치하는 사용자가 존재하는 경우, 그 사용자의 email 값을 반환한다. 찾지 못한 경우 None을 반환한다.
profile
2023-2 오소플

0개의 댓글