
Ho Eat (호잇)
2022.08풀스택 개발
프로젝트 개요
사람들이 자신의 음식레시피를 공유하고, 소통하며, 인기 순위를 확인할 수 있는 플랫폼입니다.
수행 업무
데이터 수집
- RPA(uipath)를 사용하여 회원의 더미데이터를 생성
- 레시피와 이미지를 수집하여 데이터베이스에 저장
인기 레시피
- 메인페이지 음식 카테고리 Best20 출력
- 가장 많은 조회수를 가진 Best100을 카테고리 별로 조회 하는 페이지 구현
인기 레시피 차트
- 카테고리 별 총 조회수와 평균조회수를 c3.js를 이용하여 차트로 보여주고, 카테고리 클릭 시 해당 카테고리의 Best100 화면으로 이동하는 기능 구현
인기 검색어
- 인기 검색어 순위를 조회하고 검색어 클릭 시 해당하는 레시피 검색 및 조회하는 기능 구현
ERD


주요 기능 구현 설명
회원 더미데이터 생성
1. 엑셀에서 이름데이터를 받아 리스트에 저장

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

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

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


레시피 더미데이터 생성
1. 웹 스크래핑으로 제목, 조회수, 이미지 url 수집
게시물의 url을 수집 후 해당 url로 이동하여 이미지url과 정확한 조회수를 수집했습니다.

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


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


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


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

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


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




인기 레시피 - 가장 많은 조회수를 가진 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)
}
});
});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})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)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()검색어 순위

<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>- 분류 탭 클릭 시 데이터 비동기 호출
$('#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)
}
});
});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})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카테고리 별 조회 수 차트
차트의 데이터 클릭 시 해당 카테고리 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)
}
});
}
});
});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>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'})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레슨런
상용 서비스 수준의 UI 분석 및 구현 경험
‘만개의 레시피’의 복잡한 웹 레이아웃을 직접 분석하고 클론 코딩을 진행한 경험이 있습니다. 이를 통해 HTML과 CSS의 동작 원리, 그리고 구조화된 마크업에 대한 이해를 깊이 있게 다질 수 있었습니다. 이러한 과정을 거치면서 각기 다른 환경에서도 안정적으로 동작하는 견고한 UI를 구현하는 역량을 키웠습니다.
사용자 친화적인 데이터 시각화 경험
C3.js 라이브러리를 활용하여 데이터를 직관적으로 이해할 수 있는 차트와 그래프로 시각화하는 작업을 수행하였습니다. 이 과정에서 프론트엔드 단계에서 데이터를 가공하고, 사용자에게 의미 있는 정보로 효과적으로 보여주는 데이터 핸들링 노하우 또한 습득하였습니다.
복잡한 쿼리 설계 및 성능 최적화에 대한 인식
단순한 데이터 조회를 넘어, 복잡한 요구사항을 충족하는 다중 테이블 조인과 집계 쿼리도 직접 작성해보며 RDBMS의 구조적 이해를 높였습니다. 특히 쿼리 작성 시 병목 현상을 고려하고, 조회 성능을 최적화하는 데이터베이스 튜닝의 기본기를 다졌다는 점에서 큰 성장을 느꼈습니다.