본문 바로가기
Ho Eat (호잇)

Ho Eat (호잇)

2022.08풀스택 개발

프로젝트 개요

사람들이 자신의 음식레시피를 공유하고, 소통하며, 인기 순위를 확인할 수 있는 플랫폼입니다.

수행 업무

데이터 수집

  • RPA(uipath)를 사용하여 회원의 더미데이터를 생성
  • 레시피와 이미지를 수집하여 데이터베이스에 저장

인기 레시피

  • 메인페이지 음식 카테고리 Best20 출력
  • 가장 많은 조회수를 가진 Best100을 카테고리 별로 조회 하는 페이지 구현

인기 레시피 차트

  • 카테고리 별 총 조회수와 평균조회수를 c3.js를 이용하여 차트로 보여주고, 카테고리 클릭 시 해당 카테고리의 Best100 화면으로 이동하는 기능 구현

인기 검색어

  • 인기 검색어 순위를 조회하고 검색어 클릭 시 해당하는 레시피 검색 및 조회하는 기능 구현

ERD

ERD1
ERD2
ERD

주요 기능 구현 설명

회원 더미데이터 생성

1. 엑셀에서 이름데이터를 받아 리스트에 저장

이름 데이터 엑셀

2. member table 에 들어갈 이름을 랜덤으로 이름을 생성

랜덤 이름 생성

3. 범위를 설정 후 랜덤으로 생년월일을 생성

랜덤 생년월일 생성

4. 반복문을 이용, 데이터 테이블에 회원정보를 추가 후 오라클db에 insert

회원정보를 db에 저장
더미 사용자 생성 결과
더미 사용자 생성 결과

레시피 더미데이터 생성

1. 웹 스크래핑으로 제목, 조회수, 이미지 url 수집

게시물의 url을 수집 후 해당 url로 이동하여 이미지url과 정확한 조회수를 수집했습니다.

웹 스크래핑을 활용한 레시피 정보 수집

2. 스크래핑한 데이터를 엑셀에 저장

스크래핑한 데이터를 엑셀에 작성
스크래핑한 데이터가 작성된 엑셀
스크래핑한 데이터가 작성된 엑셀

3. excel에 저장한 url을 list로 받은 후 이미지 다운로드

레시피 이미지 다운로드
레시피 이미지 다운로드 결과 이미지
레시피 이미지 다운로드 결과

4. 다운로드한 이미지파일의 이름을 다시 엑셀에 저장

레시피 이미지를 엑셀에 작성
레시피 이미지를 엑셀에 작성 결과 이미지
레시피 이미지를 엑셀에 작성 결과

5. 회원가입 되어있는 아이디와 엑셀에 저장된 정보를 DataTable에 저장

데이터를 Data Table에 추가

6. 레시피 더미데이터를 오라클 db에 저장63

데이터를 db에 적재
레시피 더미데이터 db적재 결과 이미지
레시피 더미데이터 db적재 결과

인기 레시피 - 메인페이지 음식 카테고리 Best20 출력

메인페이지 음식 카테고리 Best20 화면
메인페이지 음식 카테고리 Best20 화면
메인페이지 음식 카테고리 Best20 main.html
메인페이지 음식 카테고리 Best20 main.html
메인페이지 음식 카테고리 Best20 쿼리
메인페이지 음식 카테고리 Best20 쿼리 - HoEat/config/models.py
메인페이지 음식 카테고리 Best20 조회 데이터 정제
메인페이지 음식 카테고리 Best20 조회 데이터 정제

인기 레시피 - 가장 많은 조회수를 가진 Best100 출력

카테고리 별로 순위 조회 가능하도록 개발했습니다.

레시피 랭킹 Best100 페이지1
레시피 랭킹 Best100 페이지2
레시피 랭킹 Best100 페이지

1. 카테고리 탭 클릭 시 데이터 비동기 호출

$('#KorCook').click(function(){
  $('#TotalCook').attr('class','');
  $(this).attr('class','active');
  $('#WestCook').attr('class','');
  $('#ChiCook').attr('class','');
  $('#JapCook').attr('class','');
 
  $.ajax({
      url: "rankingRecipeCate?cate=한식",
      success: function(data){
          console.log(data)
          $('#rankingAjCate').html(data)
      }
  });
});
HoEat/ranking/templates/ranking/rankingRecipeAj.html
def rankingmain(request):
    ref = rankinOra()
    cate = request.GET.get('cate', '')
    if len(cate) > 0:
        res = ref.mainRank(cate)
        return render(request, 'ranking/rankingmain.html', {'cateRanList': res, 'cate': cate})
    res = ref.mainRank('식')
    request = sessionStart(request)
    return render(request, 'ranking/rankingmain.html', {'cateRanList': res})
 
def rankingRecipe(request):
    request = sessionStart(request)
    ref = rankinOra()
    res = ref.mainRank('식')
    # , 'cate': '식'
    return render(request, 'ranking/rankingRecipeAj.html', {'cateRanList': res})
 
def rankingRecipeCate(request):
    cate = request.GET['cate']
    ref = rankinOra()
    res = ref.mainRank(cate)
    request = sessionStart(request)
    return render(request, 'ranking/rankingRecipeAjCate.html', {'cateRanList': res})
HoEat/ranking/views.py

2. 와일드카드를 이용하여 전체 혹은 특정 카테고리의 인기 레시피 목록 조회

from config.models import Conn_Ora
 
 
class rankinOra(Conn_Ora):
    def mainRank(self, cate):
        print(f"cate => {cate}")
        conn = self.myconn()
        cursor = conn.cursor()
        query = """
        select rownum, a.* from (
        select btitle, mid, 
        case when bhit >= 10000 then round(bhit/10000,1)||'만' 
        else to_char(bhit) end as bhit2, 
        bimg, bcate 
        from board where bcate like :value order by bhit desc) a where rownum <= 100
        """
        cursor.execute(query, value='%' + cate + '%')
        res = cursor.fetchall()
        cursor.close()
        conn.close()
        return res

검색어 저장

1. 검색되는 레시피가 있을 경우에만 db에 저장

  count = "".join(str(recipeBoard.countBoard(searchType, searchValue))).strip('(,)')
  if count != '0' :
      storeSD = searchData()
      storeSD.searchToDb(searchType, searchValue)
HoEat/recipeBoard/page_search.py

2. 한국어 혹은 영어 검색 시 검색어 저장 분기 처리

  • 영어로 검색할 시 konply가 적용되지 않아 그대로 db에 저장
  • 한국어로 검색할 시 konlpy.tag.Okt를 사용 명사를 추출하여 db에 저장
class searchData:
    def searchToDb(self, sType, sValue):
        if len(sValue) > 0:
            okt = Okt()
            nouns = okt.nouns(sValue)
            print(f"nouns.count: {len(nouns)}")
            if len(nouns) < 1:
                noun = sValue
                print(f"noun: {noun}")
                sdb = SearchedData(sType=sType, snouns=noun, sdate=datetime.now())
                sdb.save()
            else:
                print(f"nouns: {nouns}")
                for i in nouns:
                    sdb = SearchedData(sType=sType, snouns=i, sdate=datetime.now())
                    sdb.save()
HoEat/ranking/models.py

검색어 순위

검색어 순위 화면
검색어 순위 화면
<ul class="most_search">
    {% for rankN, rankObject in searchRankList %}
    <li>
        <div class="best_cont">
            <p class="ranking_num st2">
                <b>{{rankN}}</b>
            </p>
            <a href="/recipeBoard/boardList?page=1&searchValue={{rankObject.snouns}}&searchType={{rankObject.sType}}">{{rankObject.snouns}}</a>
        </div>
    </li>
    {% endfor %}
</ul>
HoEat/ranking/templates/ranking/rankingSearchAjRange.html
  1. 분류 탭 클릭 시 데이터 비동기 호출
$('#TitleSearch').click(function(){
    $('#TotalSearch').attr('class','');
    $(this).attr('class','active');
    $('#WriterSearch').attr('class','');
    $.ajax({
        url: "rankingSearchRange?sType=btitle",
        success: function(data){
            console.log(data)
            $('#rankingSearchAjRange').html(data)
        }
    });
});
$('#WriterSearch').click(function(){
    $('#TotalSearch').attr('class','');
    $('#TitleSearch').attr('class','');
    $(this).attr('class','active');
    $.ajax({
        url: "rankingSearchRange?sType=mid",
        success: function(data){
            console.log(data)
            $('#rankingSearchAjRange').html(data)
        }
    });
});
HoEat/ranking/templates/ranking/rankingSearchAj.html
def rankingSearch(request):
    ref = searchData()
    res = ref.getSDBRank('')
    rankN = 1
    rankList = []
    for i in res:
        rankList.append(tuple([rankN, i]))
        rankN += 1
    for n, s in rankList:
        print(f"{n} : {s.snouns}")
    request = sessionStart(request)
    return render(request, 'ranking/rankingSearchAj.html', {'searchRankList': rankList})
 
 
def rankingSearchRange(request):
    sType = request.GET.get('sType', '')
    print("sType:", sType)
    ref = searchData()
    res = ref.getSDBRank(sType)
    rankN = 1
    rankList = []
    for i in res:
        rankList.append(tuple([rankN, i]))
        rankN += 1
    request = sessionStart(request)
    return render(request, 'ranking/rankingSearchAjRange.html', {'searchRankList': rankList})
HoEat/ranking/views.py

2. DB에서 검색어 순위 조회

class searchData:
    def getSDBRank(self, sType):
        sType = '%'+sType+'%'
        print("sType:", sType)
        sValues = SearchedData.objects.raw(f"""
        select snouns, sType, count(*) as cnt, sidx from ranking_searcheddata where sType like \'{sType}\' group by snouns order by cnt desc
        """)
        return sValues
HoEat/ranking/models.py

카테고리 별 조회 수 차트

차트의 데이터 클릭 시 해당 카테고리 best100으로 이동하도록 했습니다.

차트 화면과 클릭 이벤트 워크플로우
차트 화면과 클릭 이벤트 워크플로우

1. 카테고리 탭 클릭 및 총/평균 조회 선택 시 데이터 비동기 호출

$(function(){
    $('#viewSubmit').click(function(){
        console.log($('#param').val())
        if($('#param').val() == "default"){
            confirm("메뉴를 선택하세요");
            $('#sumOrAvgDefault').focus();
        }else{
            var param = $('#param').val();
            var dateRange = $('#dateRange').val();
            $.ajax({
                url: "cateHitRanking?param="+param+"&dateRange="+dateRange,
                success: function(data){
                    console.log(data)
                    $('#target').html(data)
                }
            });
        }
    });
});
HoEat/ranking/templates/ranking/chartRankCate.html

2. c3.js 를 사용한 차트 렌더링

<div id="chart_donut">
 
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.min.js"></script>
<script>
    var titleMsg = "";
  if("{{sumOrAvg}}" === "sum"){
    titleMsg = "카테고리 별 총 조회수";
  }else{
    titleMsg = "카테고리 별 평균 조회수";
  }
  var objectTest = [];
  {% for cate, hit in hitChart %}
    objectTest.push(['{{cate}}',{{hit}}]);
  {% endfor %}
  var chart = c3.generate({
    bindto : '#chart_donut',
    data: {
        columns: objectTest,
        type : 'donut',
        onclick: function (d, i) { console.log("onclick", d, d.id, i); location="/ranking/rankingmain?cate="+d.id},
        onmouseover: function (d, i) { console.log("onmouseover", d, i); },
        onmouseout: function (d, i) { console.log("onmouseout", d, i); }
    },
    donut: {
        title: titleMsg
    }
});
</script>
HoEat/ranking/templates/ranking/chartRankCateAj.html

3. 총/평균 조회 수 파라미터에 따른 분기 처리

def cateHitRanking(request):
    sumOrAvg = request.GET.get('param', '')
    ref = GetChart()
    if len(sumOrAvg) > 0:
        if sumOrAvg == 'sum':
            res = ref.cateHitSumRanking()
            return render(request, 'ranking/chartRankCateAj.html', {"hitChart": res, 'sumOrAvg': 'sum'})
        elif sumOrAvg == 'avg':
            res = ref.cateHitAvgRanking()
            return render(request, 'ranking/chartRankCateAj.html', {"hitChart": res, 'sumOrAvg': 'avg'})
    res = ref.cateHitSumRanking()
    return render(request, 'ranking/chartRankCate.html', {"hitChart": res, 'sumOrAvg': 'sum'})
HoEat/ranking/views.py
class GetChart(Conn_Ora):
    def cateHitAvgRanking(self):
        sql = """
        SELECT bcate, round(avg(bhit),2) AS avg_hit FROM board GROUP by bcate order by avg_hit desc
        """
        conn = self.myconn()
        cursor = conn.cursor()
        cursor.execute(sql)
        res = cursor.fetchall()
        cursor.close()
        conn.close()
        return res
 
    def cateHitSumRanking(self):
        sql = """
        SELECT bcate, sum(bhit) AS total_hit FROM board GROUP by bcate order by total_hit desc
        """
        conn = self.myconn()
        cursor = conn.cursor()
        cursor.execute(sql)
        res = cursor.fetchall()
        cursor.close()
        conn.close()
        return res
HoEat/blob/main/HoEat/ranking/models.py

레슨런

상용 서비스 수준의 UI 분석 및 구현 경험

‘만개의 레시피’의 복잡한 웹 레이아웃을 직접 분석하고 클론 코딩을 진행한 경험이 있습니다. 이를 통해 HTML과 CSS의 동작 원리, 그리고 구조화된 마크업에 대한 이해를 깊이 있게 다질 수 있었습니다. 이러한 과정을 거치면서 각기 다른 환경에서도 안정적으로 동작하는 견고한 UI를 구현하는 역량을 키웠습니다.

사용자 친화적인 데이터 시각화 경험

C3.js 라이브러리를 활용하여 데이터를 직관적으로 이해할 수 있는 차트와 그래프로 시각화하는 작업을 수행하였습니다. 이 과정에서 프론트엔드 단계에서 데이터를 가공하고, 사용자에게 의미 있는 정보로 효과적으로 보여주는 데이터 핸들링 노하우 또한 습득하였습니다.

복잡한 쿼리 설계 및 성능 최적화에 대한 인식

단순한 데이터 조회를 넘어, 복잡한 요구사항을 충족하는 다중 테이블 조인과 집계 쿼리도 직접 작성해보며 RDBMS의 구조적 이해를 높였습니다. 특히 쿼리 작성 시 병목 현상을 고려하고, 조회 성능을 최적화하는 데이터베이스 튜닝의 기본기를 다졌다는 점에서 큰 성장을 느꼈습니다.