본문 바로가기
Record/WIL

WIL(20230327 ~ 20230402), 미니 프로젝트(오늘의 꿀)

by junhub 2023. 3. 30.

기간 : 2023.03.27 ~ 2023.03.30

프로젝트명 : 오늘의 꿀 

프로젝트 설명 : 오늘의집에서 사용해본 아이템을 추천하는 웹페이지 

 

 

 

# 구현한 기능 

1. 사용자가 상품 URL, 가격, 카테고리, 후기 입력 후 등록하기 버튼 클릭 시 서버와 연결해서 db에 저장 후 저장된 db를 웹페이지에 HTML 카드로 표현

2. 가격 및 카테고리 조건에 따라 검색 기능 구현 

3. 구매버튼 클릭 시 해당 카드 제품의 구매 링크로 이동 

4. 카드 내에 하트 버튼 클릭 시 누른 횟수만큼 하트 숫자가 1씩 카운트 되는 기능 구현 

5. title 배너 클릭 시 localhost 초기 페이지로 이동 

 

 

# 사용한 라이브러리 

flask, pymongo, dnspython, requests, bs4 

 

 

 

# 내가 구현한 기능 

 

1. 사용자가 상품 URL, 가격, 카테고리, 후기 입력 후 등록하기 버튼 클릭 시 서버와 연결해서 db에 저장 후 저장된 db를 웹페이지에 HTML 카드로 표현 

function posting() {           

            let url = $('#url').val()
            let price = $('#price').val()
            let category = $('#category').val()
            let comment = $('#comment').val()

            if (price == '-- 금액 --' || category == '-- 카테고리 --') {
                alert('금액과 카테고리를 선택하세요')
            }
            else{
            let formData = new FormData();
            formData.append("url_give", url)
            formData.append("price_give", price)
            formData.append("category_give", category)
            formData.append("comment_give", comment)

            fetch('/product', { method: "POST", body: formData }).then((res) => res.json()).then((data) => {
                alert(data['msg'])
                window.location.reload()
            })  
            }                         
        }

등록하기 버튼을 클릭하면 posting 함수가 실행되면서 입력받은 값을 val() 메소드를 사용해 각 변수에 할당한다. 이때 조건문을 사용해 가격 혹은 카테고리 둘중 하나라도 고르지 않으면 '금액과 카테고리를 선택하세요' 라는 alert 창이 나타나게 했고, 그렇지 않다면(둘다 선택했다면) formData = new FormData() 를 사용해 데이터를 담아서 POST 방식으로 /product(백엔드)에 보내게 코드를 구성했다. 

 

@app.route("/product", methods=["POST"])
def product_save():
    url_receive = request.form['url_give']
    price_receive = request.form['price_give']
    category_receive = request.form['category_give']
    comment_receive = request.form['comment_give']

    headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
    data = requests.get(url_receive, headers=headers)
    soup = BeautifulSoup(data.text, 'html.parser')

    ogtitle = soup.select_one('meta[property="og:title"]')['content']
    ogimage = soup.select_one('meta[property="og:image"]')['content']
    ogurl = soup.select_one('meta[property="og:url"]')['content']

    all_products = list(db.product.find({},{'_id':False}))
    num = len(all_products)

    doc = {
        'num':num,
        'category':category_receive,
        'comment':comment_receive,
        'price':price_receive,
        'image':ogimage,
        'title':ogtitle,
        'url':url_receive,
        }

    db.product.insert_one(doc)
        
    return jsonify({'msg':'등록 완료!'})

프론트에서 보내준 데이터를 받아 각 변수에 다시 할당하고, 웹 크롤링을 통해 title, image, url을 가져온다. 이때 프론트에서 넘겨받은 url을 data = requests.get(url_receive, headers=headers)에서 사용하여 크롤링을 해온다. 

 

list(db.product.find({},{'_id':False}))를 통해 db에 저장된 모든 값을 가져와서 all_products에 할당하고 len 함수를 사용해 all_products의 길이를 num에 할당하는데, db에 들어있는 문서의 개수 즉, 몇개의 상품이 들어있는지 num에 할당하는 것이다. 

만약 첫번째 상품을 등록했다고 하면 해당 부분에선 아직 db에 데이터가 업로드 되지 않았으므로 len(all_products)의 값은 0 일 것이다. 

이렇게 num을 상품의 내용과 같이 db에 업로드 하는 이유는 각 상품에 고유 번호를 부여하기 위함이다. 

 

다음으로 doc에 딕셔너리 형태의 데이터를 담아 key : value 형태로 db에 업로드한다. 이때 업로드 되는 value 값은 프론트에서 넘어온 데이터와 크롤링을 통해 얻어온 데이터를 올리게 된다. 

 

insert_one() 을 사용해 db에 올리게 되고, 프론트로 'msg' : '등록 완료!' data를 return 한다. 

 

$(document).ready(function () {
            listing();
        });

        function increment(num) {
            var target = num;
            let count = Number($('#'+target).val());           
            count += 1;
            $('#'+target).val(count);
        }

        function listing() {
            fetch("/product").then((res) => res.json()).then((data) => {
                let rows = data['result']
                $('#cards-box').empty()
                rows.forEach((a) => {
                    let category = a['category']
                    let comment = a['comment']
                    let price = a['price']
                    let image = a['image']
                    let title = a['title']
                    let url = a['url']
                    let num = a['num']


                    let temp_html = `<div class="col">
                                    <div class="card h-100">
                                        <img src="${image}"
                                            class="card-img-top">
                                        <div class="card-body">
                                            <h5 class="card-title">${title}</h5>
                                            <p class="category">카테고리 : ${category}</p>                                          
                                            <p class="price">가격 : ${price}</p>
                                            <p class="mycomment">한줄평 : ${comment}</p>                                            
                                            <br></br>
                                            <br></br>                                       
                                            <input type='button' id=${num} class='heart' onclick="increment(${num})" value=0>                                        
                                            <div class=linkbt>                                           
                                            <a href=${url} target="_blank" button type="button" class="btn btn-outline-dark" onclick="link()">구매하기</button>
                                            </a>
                                            </div>
                                        </div>
                                    </div>`
                    $('#cards-box').append(temp_html)
                })
            })
        }

위 코드로 인해 웹페이지가 동작하자마자(혹은 새로고침) listing() 함수가 실행되게 된다. listing() 함수 내 Fetch에는 별다른 method가 보이지 않는데 Fetch는 method 를 지정해주지 않았을경우 default 가 GET 방식이기 때문이다. 

 

/product로 신호를 보낸 후 데이터를 받아오게 되는데, 해당하는 백엔드 코드는 다음과 같다. 

@app.route("/product", methods=["GET"])
def product_get():
    all_products = list(db.product.find({},{'_id':False}))
    return jsonify({'result':all_products})

list(db.product.find({},{'_id':False}))를 통해 모든 db 데이터를 all_products에 할당하고 'result' : all_products 형태로 프론트에 return 하게 된다. 

 

다시 프론트쪽 코드를 보면 data['result']    =====>    all_products 를 의미하고 row에 할당했다. 그 후 반복문이 시작되기 전에 empty() 메소드를 사용해 카드를 모두 지워줬고, forEach 반복문을 사용해 백엔드에서 받아온 데이터를 하나씩 반복해서 카드에 붙이게 된다. temp_html이라는 변수에 카드가 생성되는 html 코드를 할당해주고 jquery 문법을 사용해 유동적으로 바뀌어야 되는 값들을 변수로 채워줬다. 마지막으로 append 메소드를 통해 temp_html에 할당된 html 코드를 붙여줬다(카드 생성)

 

현재까지 설명한 코드들은

                                        $(document).ready(function () {

                                                    listing(); 

                                        }); 

함수를 통해 웹페이지가 동작하자마자(혹은 새로고침) 실행된다. 


2. 가격 및 카테고리 조건에 따라 검색 기능 구현 

 

function search() {
            let price = $('#price').val()
            let category = $('#category').val()

            if (price == '-- 금액 --' || category == '-- 카테고리 --') {
                alert('금액과 카테고리를 선택하세요')
            }

            let formData = new FormData();
            formData.append("price_give", price)
            formData.append("category_give", category)

            fetch('/product/search', { method: "POST", body: formData }).then((res) => res.json()).then((data) => {
                let rows = data['result']
                $('#cards-box').empty()
                rows.forEach((a) => {
                    let category = a['category']
                    let comment = a['comment']
                    let price = a['price']
                    let image = a['image']
                    let title = a['title']
                    let url = a['url']

                    let temp_html = `<div class="col">
                                    <div class="card h-100">
                                        <img src="${image}"
                                            class="card-img-top">
                                        <div class="card-body">
                                            <h5 class="card-title">${title}</h5>
                                            <p class="category">카테고리 : ${category}</p>                                          
                                            <p class="price">가격 : ${price}</p>
                                            <p class="mycomment">한줄평 : ${comment}</p>                                            
                                            <br></br>
                                            <br></br>                                       
                                            <input type='button' class="heart" onclick="increment()" value=0>                                        
                                            <div class=linkbt>                                           
                                            <a href=${url} target="_blank" button type="button" class="btn btn-outline-dark" onclick="link()">구매하기</button>
                                            </a>
                                            </div>
                                        </div>
                                    </div>`
                    $('#cards-box').append(temp_html)
                })
            })
        }

검색 버튼을 누르면 search() 함수가 실행된다. 입력받은 price 와 category 값을 변수에 할당하고, 조건문 내에 연산자를 활용해 만약 둘중 하나라도 선택하지 않았다면 '금액과 카테고리를 선택하세요' alert 창이 뜨게하고, 그게 아니라면(둘다 선택했다면) 입력받은 데이터를 할당한 price와 category를 백엔드 /product/search 로 보내주게 된다. 

 

@app.route("/product/search", methods=["POST"])
def product_search(): 
    price_receive = request.form['price_give']
    category_receive = request.form['category_give']
    
    all_products = list(db.product.find({},{'_id':False}))
    docs = [doc for doc in all_products if doc['price'] == price_receive and doc['category'] == category_receive]

    return jsonify({'result':docs})

프론트에서 받아온 price_give 와 category_give를 변수에 할당하고, 모든 db 데이터를 all_products에 할당한다. 

그 후 반복문을 사용해 all_products의 값을 doc에 하나씩 할당하면서 'price' 와 'category' 의 value 값이 각각 price_receive 와 category_receive와 같은지 비교하고 같다면 docs에 할당하게 된다. 이러한 과정으로 사용자가 웹페이지에서 선택한 조건에 따라 db에서 일치하는 데이터를 꺼내오게 된다. 마지막으로 조건에 일치한 데이터를 할당한 docs를 프론트로 return 해준다.

 

마찬가지로 받은 data['result']  즉, docs를 반복문을 사용해 a에 하나씩 할당하면서 필요한 데이터를 꺼낸 후 카드의 내용에 해당하는 부분에 변수로 넣어 카드 형식의 html을 append 메소드를 사용해 웹페이지에 표시하게 된다. 

 

반복문이 돌아가기 전에 empty 메소드를 사용해 기존 카드를 모두 지워줬으므로, 백에서 받은 docs에 해당하는 데이터만 카드 형식으로 나타나게 된다. 


3. 구매버튼 클릭 시 해당 카드 제품의 구매 링크로 이동

let temp_html = `<div class="col">
                                    <div class="card h-100">
                                        <img src="${image}"
                                            class="card-img-top">
                                        <div class="card-body">
                                            <h5 class="card-title">${title}</h5>
                                            <p class="category">카테고리 : ${category}</p>                                          
                                            <p class="price">가격 : ${price}</p>
                                            <p class="mycomment">한줄평 : ${comment}</p>                                            
                                            <br></br>
                                            <br></br>                                       
                                            <input type='button' id=${num} class='heart' onclick="increment(${num})" value=0>                                        
                                            <div class=linkbt>                                           
                                            <a href=${url} target="_blank" button type="button" class="btn btn-outline-dark" onclick="link()">구매하기</button>
                                            </a>
                                            </div>
                                        </div>
                                    </div>`
                    $('#cards-box').append(temp_html)

href를 사용해서 해당 카드의 url를 할당한 url 변수를 jquery로 넣어 버튼 클릭 시 자동으로 링크가 활성화되게 했다.


4. 카드 내에 하트 버튼 클릭 시 누른 횟수만큼 하트 숫자가 1씩 카운트 되는 기능 구현

 

function increment(num) {
            var target = num;
            let count = Number($('#'+target).val());           
            count += 1;
            $('#'+target).val(count);
        }

버튼의 내용을 보면 id = ${num}, class = 'heart' , onclick="increment(${num})" 이다. 버튼 클릭 시 increment() 함수가 작동되고 increment() 함수에 num이 매개변수로 들어가게 된다. 이때 num은 앞서 말했다시피 등록할때마다 제품에 붙여지는 고유번호이다. 

 

        function increment(num) {
            var target = num;
            let count = Number($('#'+target).val());           
            count += 1;
            $('#'+target).val(count);
        }

첫번째 카드라고 가정하면 고유번호인 num = 0 일것이다. 따라서 target(카드를 지정한다는 의미가 통하게 변수명을 지었음)에 num을 할당하고, 버튼의 코드를 보면 숫자를 value로 표현하고 있다. 이 value 값을 하트가 클릭될 때마다 변화시켜줘야 하므로 count라는 변수에 위와 같이 할당했다. $('#' + target)을 사용해 하트 버튼을 누른 카드를 지정해줬고 .val()을 사용해 val 값이 변할 수 있게 해줬다. 

그 후 Number 함수로 감쌌는데 해당 값을 정수로 변환하기 위한 목적이다. 그 후 count += 1, 버튼을 누를때 마다 기존 count 값에 +1이 되게 하고 최종적으로 val(count) 와 같이 표현하여 val 값이 count에 맞게 계속 변화하게 표현했다. 

 

  • 어려웠던 점 

버튼을 누른 해당 카드의 하트 숫자가 변화해야 되는데 어느 버튼을 눌러도 자꾸 첫번째 카드의 하트 숫자만 올라갔다. 버튼을 누른 곳의 카드를 지정해줘야 하는데 이 부분이 어려웠고, 제품을 등록할 때마다 고유 번호 num을 부여하여 문제해결에 조금 더 다가섰고 $('#' + target) 문법을 사용하여 최종적으로 해결했다. 

 

  • 하트 버튼 css 부분
        .heart {
            border: none;
            width: 100px;
            height: 100px;
            position: absolute;
            left: 230px;
            bottom: 0px;
            transform: translate(-50%, -50%);
            background: url(https://cssanimation.rocks/images/posts/steps/heart.png) no-repeat;
            background-position: 0 0;
            cursor: pointer;
            animation: fave-heart 1s steps(28);
        }

        .heart:hover {
            background-position: -2800px 0;
            transition: background 1s steps(28);
        }

5. title 배너 클릭 시 localhost 초기 페이지로 이동

 

div를 지정해서 onclick을 사용해서 클릭시 "location.href = '해당 로컬호스트의 주소'" 로 이동하게 했다.

 

오늘의 꿀 배너 클릭 시 작동한다. 


# 느낀 점 

코딩을 공부하고 처음으로 해보는 팀 프로젝트였다. 내 역할을 잘 해낼 수 있을 지 걱정이 너무 많았다.

팀원들에게 민폐 끼치고 싶지 않았고, 멍하니 내가 해야할 일을 못하고 얹혀가는건 정말 싫었다. 그래서 팀 프로젝트 시작 전까지 열심히 공부했다. 내가 맡은 몫은 잘 해내야지 라는 마음과 불안함을 해소하고자 하는 마음이 원동력이였다. 

 

결과적으로 봤을 때 이정도면 나름 성공한 거 같다. 좋은 팀원분들을 만나서 의견 공유와 협업을 트러블 없이 순탄하게 진행했고, 나 포함 모든 팀원들이 프로젝트를 진행해가면서 하루하루 성장해가는 모습을 봤다. 난생 처음해보는 공부를 바탕으로 모든게 낯선 순간에 팀원들과 하나하나 해결해가면서 같이 앞으로 나아갈 수 있는 귀한 경험이였다. 

댓글