[POTG] - 해설2

Jen Lee·2024년 12월 4일

공동구매 페이지 소개

공동구매 웹페이지는 로그인된 사용자가 상품을 등록하고, 다른 사용자들과 공동으로 구매할 수 있도록 지원하는 플랫폼이다. 사용자는 상품의 정보, 가격, 구매 마감일 등을 입력하며, 시스템은 실시간으로 데이터를 관리하고 D-Day를 계산해 표시한다. 또한 직관적인 상품 정보를 제공한다.

웹 어플리케이션 구상도

*pixso 사용

세부 기능 소개

1. 상품 등록 기능

설명: 사용자는 상품명, 가격, 카테고리, 구매 수량, 구매 마감일, 상세 설명 등을 지정하여 상품을 등록할 수 있다. 등록된 정보는 Firebase Realtime Database에 저장된다.

D-Day 계산 : 파이썬에서 현재 시각을 불러오고 .now().date()로 연,월,일 형식으로 사용자가 설정한 구매 마감일을 기준으로 현재 날짜와의 차이를 계산하여 D-0, D+1 등의 형식으로 표시한다.

개당 가격 계산 : 사용자에게 입력받은 상품의 가격과 수량을 정수 계산하여 개당 가격을 계산해 데이터베이스에 넣는다.

판매자 아이디 : 로그인된 사용자의 아이디로 자동 입력된다.

    
    # 공동구매 화면
    # 공동구매_상품등록
    def insert_gr(self, name, data, img_path, session):
        current_date=datetime.now().date()
        # 입력된 날짜 (data['date'])를 날짜 객체로 변환
        try:
            target_date = datetime.strptime(data['date'], "%Y-%m-%d").date()
        except ValueError:
            print("Invalid date format. Expected 'YYYY-MM-DD'.")
            return False
        # D-Day 계산
        d_day = (target_date - current_date).days
        d_day_display = f"D-{d_day}" if d_day > 0 else "D-Day" if d_day == 0 else f"D+{-d_day}"
        

        #개당 개수
        per_price=int(data['price'])/int(data['cnt'])

        #quantity 기본값
        initial_quantity = int(data.get('quantity', 0)) 
        

        item_info ={
        "id": session['id'],
        "category": data['category'],
        "price": data['price'],
        "info": data['info'],
        "cnt":int(data['cnt']),
        "address": data['address'],
        "date":data['date'],
        "details": data['details'],
        "img_path": img_path,
        "d_day":d_day_display,
        "per_price":per_price,
        "quantity": initial_quantity,
        "updated_cnt": data['cnt']
        }
        self.db.child("gr_item").child(name).set(item_info)
        print(data,img_path)
        return True

2. 상품 리스트 및 상세 화면

[1] 상품 리스트

  • 설명: 상품 등록 후, 사용자에게 등록된 상품이 리스트 형태로 표시된다. 각 상품은 이름, 가격, D-Day 정보가 렌더링되며, 클릭 시 참여하기 버튼을 누를 시 상세 페이지로 이동한다.

해당 상품의 정보를 database에서 불러와 전체화면에 상품별 표시하는 html코드

<div class="product-card">
  <h2>{{ product.name }}</h2>
  <p>Price: {{ product.price }}</p>
  <p>D-Day: {{ product.d_day }}</p>
</div>

[2] 상품 상세 화면

  • firebase에서 상품에 대한 정보를 받아와 페이지를 구성한다.
  • 하단의 수량은 최대 주문 가능 수량까지 선택 가능하며 수량을 고르면 이에 따라 총 금액이 변경된다.

수량 및 총 금액 변경하는 js코드

<script>
        document.addEventListener('DOMContentLoaded', function () {
            const price = {{data.price}};// 총 가격
            const cnt = {{data.cnt}};   // 총 개수
    
            // 개당 가격 계산
            const perPrice = Math.floor(price / cnt);
            document.getElementById('per-price').textContent = perPrice;
    
            const quantityInput = document.getElementById('quantity');
            const totalPriceElement = document.getElementById('total-price');
    
            // 수량 변경 시 총 금액 업데이트
            quantityInput.addEventListener('input', function () {
                const quantity = parseInt(quantityInput.value) || 0;
                const totalPrice = quantity * perPrice;
                totalPriceElement.textContent = `총 금액 : ${totalPrice}`; // 3자리 콤마 추가
            });
        });
    </script>

상세 화면에서 수량에 맞춰 공동구매에 참여하고 수량을 db에 업데이트

# app.py
# 공동구매 수량 db에 등록
@application.route("/gr_quantity", methods=['POST'])
def gr_quantity():
   data = {
           'name': request.form['name'],
           # 'quantity': int(request.form['quantity']),
           'cnt': int(request.form['cnt'])  # 주문 가능 수량이 필요하면 추가
       }
   inputCnt = request.form['quantity']
   # 데이터 업데이트
   DB.update_quantity(data, inputCnt)
   # updated_item = DB.db.child("gr_item").child(data['name']).get().val()
   return redirect(url_for('grpPurchase'))
   
# database.py
# 공동구매 수량 등록
   def update_quantity(self, data, inputCnt):
       #현재 데이터 가져오기
       current_item = self.db.child("gr_item").child(data['name']).get().val()
       # 남은 수량 계산
       updated_quantity = int(current_item['quantity']) + int(inputCnt) #누적 개수
       remain_cnt = int(current_item['cnt']) - updated_quantity #남은 개수
       updated_item = {
           "updated_cnt" : remain_cnt,
           "quantity": updated_quantity
       }
       current_item.update(updated_item)
       self.db.child("gr_item").child(data['name']).set(current_item)
       return True

  • 다음과 같이 사용자가 참여한 공동구매 수량이 반영되어 전체 페이지에서 해당 상품의 구매 확정 개수가 변화한다. >> 상세페이지에서도 구매가능한 수량은 2로 감소한다.

[3] 핵심 설명

  • 누적된 주문 수량 : updated_quantity = int(current_item['quantity']) + int(inputCnt)로 계산하며 사용자들이 입력한 주문 수량을 더하여 데이터를 업데이트한다.
  • 주문 가능 수량 : updated_cnt로 remain_cnt = int(current_item['cnt']) - updated_quantity 누적된 주문 수량을 기존의 사용자가 입력한 수량에 빼서 업데이트한다.

3. 카테고리 및 좌측 진행률 필터

설명 : 사용자는 상품을 상단의 섹션으로 카테고리별로 탐색할 수 있으며, 좌측 필터를 사용해 진행률(전체 수량 중 다른 사용자가 참여한 수량의 비율) 기준으로 상품을 세부적으로 필터링할 수 있습니다. 위 기능들은 사용자가 원하는 상품을 더 빠르게 찾을 수 있도록 하는 요소이다.

[1] 카테고리(사용자 입력 기준)

  • 상단 카테고리 섹션에서 버튼을 클릭하면 해당 카테고리에 해당하는 상품만 화면에 표시된다.
  • 선택된 카테고리는 활성화 상태로 표시된다.


해당 html 코드 및 css 코드

<section class="category-section">
   <form method="get" action="/grpurchase_ViewAll">
       <button type="submit" name="category" value="" class="category-item category-title" style="margin: 0 auto;"><h2 class="category-name">카테고리</h2></button>
       <div class="category-container">
           {% for category in ["화장품", "의류", "육류 및 해산물", "가공식품", "과일 및 채소", "냉동식품", "전자제품", "기타"] %}
               <button 
                   type="submit" 
                   name="category" 
                   value="{{ category }}" 
                   class="category-item {% if current_category == category %}active{% endif %}">
                   {{ category }}
               </button>
           {% endfor %}
       </div>
   </form>
</section>
<style>
.category-item.active:not(.category-title) {
   background-color: rgb(0, 70, 42);
   color: white;
}
</style>

해당 카테고리 상품 라우팅하는 app.py 코드

def view_product():
    page = request.args.get("page", 0, type=int)
    category = request.args.get("category", "all") 
    #화면에서 셀렉트박스 선택한 카테고리 값 받아오기
	# 카테고리별로 db에서 데이터 받아오기
    if category!="all":
        data = DB.get_items_bycategory(category)
        item_counts = len(data)

[2] 좌측 필터

  • 진행률 필터를 통해 특정 비율 이상 완성된 공동구매 상품만 표시 가능하다.

해당 html 코드 및 css 코드

    <aside class="filter-section">
        <a href="/view_grReg" class="open-gr">공동구매 등록</a>
        <h3>필터</h3>
        <hr>
        <form method="get" action="/grpurchase_ViewAll">
            <div class="filter-group">
                <h3>진행사항</h3>
                <div class="filter-item">
                    <input type="checkbox" id="9" name="progress_filter" value="90">
                    <label for="9">90%~</label>
                </div>
                <div class="filter-item">
                    <input type="checkbox" id="8" name="progress_filter" value="80">
                    <label for="8">80%~</label>
                </div>
                <div class="filter-item">
                    <input type="checkbox" id="7" name="progress_filter" value="70">
                    <label for="7">70%~</label>
                </div>
                <div class="filter-item">
                    <input type="checkbox" id="6" name="progress_filter" value="60">
                    <label for="6">60%~</label>
                </div>
                <div class="filter-item">
                    <input type="checkbox" id="5" name="progress_filter" value="50">
                    <label for="5">50%~</label>
                </div>
                <div class="filter-item">
                    <input type="checkbox" id="4" name="progress_filter" value="0-50">
                    <label for="4">~50%</label>
                </div>
            </div>
            <button type="submit" class = "open-gr" style="font-size: 14px; float: left; border: none;">필터 적용</button>
        </form>        
    </aside>

해당 카테고리 상품 라우팅하는 app.py 코드

# 진행률 필터
    if progress_filter:
        filtered_data = {}
        for key, item in data.items():
            progress = (item['quantity'] / item['cnt']) * 100 if item['cnt'] > 0 else 0
            for filter_value in progress_filter:
                if filter_value == "0-50":
                    if progress <= 50:
                        filtered_data[key] = item
                else:
                    try:
                        if progress >= int(filter_value):
                            filtered_data[key] = item
                    except ValueError:
                        pass  # 예상치 못한 값은 무시
        data = filtered_data
    

[3] 핵심 설명

  • 카테고리 필터 : data = {k: v for k, v in data.items() if v['category'] == category} / 선택된 카테고리에 해당하는 상품만 필터링한다.
  • 진행률 필터 : progress = (item['quantity'] / item['cnt']) * 100 if item['cnt'] > 0 else 0 / 진행률은 '사용자들에 의해 선택된 수량 / 기존의 등록자가 작성한 총 수량'이며, 상품 진행률을 계산 후 조건에 맞는 상품만 필터링한다.

개발 환경 소개

  • 개발 언어: Python (Flask), JavaScript, HTML, CSS
  • 데이터베이스: Firebase Realtime Database
  • 배포 환경: Flask 서버, Firebase 호스팅
  • 도구 및 라이브러리:
    Pyrebase: Firebase와의 통신을 지원하는 Python 라이브러리
    Flask: 백엔드 웹 프레임워크
    datetime: 날짜 및 시간 계산용 Python 모듈 >> d-day 계산시 유용
import pyrebase
import json
from datetime import datetime

기술/OSS/API 소개

1. firebase realtime database

실시간 데이터 동기화를 지원하는 NoSQL 기반의 클라우드 데이터베이스로 데이터는 JSON 트리 형태로 저장되며,경로를 통해 데이터에 접근한다. 공동구매에서는 gr_item/{상품명} 경로에 상품 데이터를 저장한다.

경로를 통해 데이터에 접근하며

# 데이터 가져오기
    data = DB.gr_get_items()

접근한 데이터를 data.{데이터명}의 형태로 html코드에서 활용한다.

<div class="a-info-row">
                      
    <span class="title">거래 장소</span>
    <span class="value">{{data.address}}</span>
</div>
<div class="divider"></div>
<div class="a-info-row">
    <span class="title">상품 정보</span>
    <span class="value">{{data.details}}</span>
</div>

gr_item/{상품명}으로 저장된 상품예시

2.Flask와 Pyrebase 연동

  • Flask에서 Pyrebase를 사용하여 Firebase API와 통신한다.
#app.py

from flask import Flask, render_template, request, flash, redirect, url_for, session, jsonify
from database import DBhandler
import hashlib
import sys
import random
application = Flask(__name__)

#database.py
import pyrebase

기타 첨언

  • firebase 경로에서의 문자 제한

    상품명에 '[브랜드명]상품명'으로 작성하였으나

    제출하기를 눌렀을때 "error" : "Invalid path: Invalid token in path"가 뜨며, 데이터 베이스에 상품이 등록되지 않는다.

결론 : firebasesms ., $, #, [, ], /와 같은 문자를 허용하지 않는다...따라서 데이터 입력할 때 설정 잘해야한다.

profile
이것저것 주워먹기

0개의 댓글