이 서비스는 사용자 정보 기반 맛집 추천 서비스로 사용자에게 적절한 매장들을 추천해 주는 기능과 여러 정보를 바탕으로 각종 보고서를 제작하여 제공해 주는 기능을 가지고 있습니다.
맛집 추천 서비스의 경우 KNN과 SVDPP, K-means 알고리즘을 복합적으로 사용하여 추천해 줍니다.
보고서의 경우 다이닝 코드, 결제 데이터, 통계청 데이터 등을 활용하여 나이대별 소비 트렌드, 지역별 경제 특성분석, 상권 분석, 체인점 분석, 업종별 경향 분석 등의 결과를 보여줍니다.
- 각종 알고리즘과 머신러닝을 기반으로 사용자에게 사용자의, 사용자를 위한, 사용자에 의한 맛집 추천 사이트
- 나만의 맛집 추천을 받고 싶은 사람들에게 맞춤 정장과 같은 느낌의 맛집 추천 시스템
- 맛집에 관한 정보 뿐만 아니라 사람들의 소비트렌드, 지역별 분석, 상권 분석, 체인점 분석, 업종별 분석 결과를 통해 경제 경향을 볼 수 있는 보고서 기능
- 무분별한 맛집 추천에 지친 30대 차도남
- 경제, 사회에 대해 관삼이 많은 여대생
- 창업에 관심이 많은 예비 창업자
- Python 3.6.8
- nodeJS 13.5.0
- npm 6.13.4
- Django 2.2.7
- djangorestframework 3.10.3
- axios 0.19.0
- vue 2.6.10
- vuex 3.1.1
- pandas 0.25.3
그 외 기타 라이브러리는 backend/requirements.txt, frontend/package.json 참조
이 프로젝트는 nginx를 활용해 frontend 정적파일 배포 및 backend uwsgi upstream 적용하였으며, Lets encrypt SSL 이용하여 받아온 인증서를 사용하여 사이트 보안 접속을 추가하였습니다.
pm2에서는 frontend와 backend의 스크립트파일을 통해 frontend서버와 backend서버를 구동해 볼 수도 있습니다.
pm2 start frontend/dps.json
pm2 start backend/dps.json
-
DB 모델 설계
[data schema](#data schema) 참조
-
Pandas DataFrame DB 마이그레이션
데이터를 DB에 넣으려면 아래와 같이 실행하면 됩니다.
cd backend python manage.py initialize ## 우분투 환경에선 python 대신 python3을 입력하면 된다.
-
검색 기능 확장
근처에 존재하는 매장들의 메뉴나 매장명을 검색하는 기능입니다. 거리를 설정할 수 있으며 현재 보고 있는 지도의 중심점에서 설정한 거리 반경 이내의 매장의 메뉴, 매장명 등을 검색할 수 있습니다.
-
유저 정보 기능 구현
유저의 상세 정보를 프론트에 제공해주기 위해 로그인 성공 시 유저 정보를 응답해줍니다.
- 추가사항: 회원탈퇴, 비밀번호 변경, 비밀번호 재설정 기능
-
음식점 정보 기능 구현
매장명에 해당하는 매장의 정보(매장 상세정보, 메뉴, 평균평점)를 반환합니다.
-
음식점 사진 자료 크롤링
서비스를 진행하는동안 자동으로 이미지가 등록되지 않은 매장을 조회 시 크롤링 할 매장 아이디와 우선순위를 저장한 딕셔너리를 갱신합니다.
크롤링 해야 할 매장의 갯수가 일정 수준 이상이 되면 자동으로 크롤링을 시작하고 크롤링 할 목록이나 크롤링 시작을 요청할 수 있는 API가 있습니다.
크롤링 할 딕셔너리 조회 API: https://i02d106.p.ssafy.io:8765/api/crawling_check
크롤링 시작 요청 API: https://i02d106.p.ssafy.io:8765/api/crawling_start
-
구글에 매장명, 주소를 검색하여 받아온 결과에서 썸네일 이미지 url을 가져와 db에 저장합니다.
-
회원가입 기능 구현
라이브러리에서 제공하는 회원가입 기능을 커스터마이징하여 추가적인 정보를 받도록 하였습니다.
- 추가사항: 이메일 인증
-
로그인, 로그아웃 기능 구현
django-rest-auth, rest_framework_jwt라이브러리를 사용하였고, 로그인 했을 경우, JWT 토큰을 프론트에 넘겨줍니다.
JWT토큰을 사용하였기 때문에 백엔드에서는 로그아웃에 대한 처리를 따로 하지않았습니다.
- 추가사항: 회원탈퇴 기능
- 완전히 유저를 삭제하지 않고 is_active 값을 조정해줌으로써 관련 댓글과 리뷰 정보는 남겨두도록 하였습니다.
-
리뷰 기능 구현
리뷰 작성 요청이 들어오면 권한이 있는지 확인 후 권한이 있으면 리뷰를 작성합니다.
리뷰가 작성이 되면 store와 user의 review_count를 각 각 1씩 더해 줍니다.
리뷰 삭제 요청이 들어오면 권한이 있는지 확인 후 권한이 있으면 리뷰를 삭제합니다.
리뷰가 삭제되면 store와 user의 review_count를 각 각 1씩 빼줍니다.
데이터 가공 -> 알고리즘 학습 -> user_based로 id에 대한 정보 선별 -> TOP10 식당 추천
-
데이터 가공
리뷰가 10개 이상인 음식점의 리뷰와 리뷰가 10개 이상인 유저 두 집합에 포함되는 리뷰의 user_id, store_id, score를 가져옵니다.
-
KNN 알고리즘 학습 및 구현
위에 전처리 한 리뷰 데이터를 기반으로 평점을 매기지 않은 음식점들 중 추정 score 값이 가장 높은 매장 상위 20개를 반환합니다.
-
데이터 가공
KNN 알고리즘 구현의 데이터 가공과 동일
-
SVDPP 알고리즘 학습 및 구현
이전에 전처리한 리뷰 데이터를 기반으로 평점을 매기지 않은 음식점들 중 추정 score 값이 가장 높은 매장 상위 20개를 반환합니다.
-
데이터 가공
리뷰가 5개이상 작성된 매장에서 리뷰를 가져옵니다. 이 중 평점이 3점 이상일 경우 좋은 리뷰로 판단합니다.
-
TF-IDF 알고리즘 구현
각 매장별로 좋은 리뷰를 TF, 각 매장별 전체 리뷰를 IDF로 사용하여 좋은 리뷰에서 나온 정보를 바탕으로 점수를 계산하여 높은 순으로 반환합니다. 이 정보는 각 매장 상세 페이지에서 워드클라우드로 확인할 수 있습니다.
-
데이터 가공
리뷰 작성 갯수가 10개 이상인 유저를 가져옵니다.
성별이 남자일 경우 gender값을 15로, 여자일 경우 0으로 바꿔 줍니다.
-
K-Means 알고리즘 구현
위에서 전처리된 데이터를 기반으로 k-means++ 알고리즘을 적용해 유저 군집 각각의 centroid를 얻어냅니다.
각 각의 군집은 유저의 성별과 나이를 바탕으로 5개의 군집을 이루도록 만들었습니다.
매장 정보가 부족하고 가공하기에 부적절하다고 생각하여 이 부분을 유저 군집을 구하여 해당 군집의 유저(나이와 성별을 바탕으로 구분된 군집들)이 주로 찾는 매장을 추천하도록 하였습니다.
리뷰 작성 데이터가 충분하지 않은 유저의 경우 knn, kmeans알고리즘을 적용하지 않고 해당 유저가 어떤 유저 군집에 속해있는지 확인하여 매장을 추천합니다.
저희 추천 시스템은 사용자의 정보(리뷰 작성량이 일정갯수 이상)가 충분할 경우 svdpp 혹은 knn알고리즘 바탕으로 리뷰를 작성하지 않은 음식점들의 평점을 추산하여 가장 높은 음식점을 추천 목록으로 제공합니다.
만약 사용자의 정보가 부족한 경우, 나이와 성별을 바탕으로 해당 유저가 어느 군집에 속해있는지 판별한 후 속하는 유저 군집에서 인기도점수가 높은 매장을 추천 목록으로 제공합니다.
기본적으로 svdpp와 knn알고리즘이 거의 동일한 역할을 하고 있기 때문에, 서버에 현재 어떤 알고리즘이 적용되어 있는지 확인하고 이를 변경할 수 있는 API를 만들어 원하는 알고리즘이 적용된 추천 리스트를 제공할 수 있도록 구성하였습니다.
관리자로 등록된 유저로 로그인 한 경우 페이지 우측 상단에 관리자 페이지로 이동 링크가 보여지게 되며 클릭 시 관리자 페이지로 이동합니다.
유저, 음식점, 리뷰 등을 보거나 삭제, 음식점 등록을 할 수 있습니다.
관리자가 아닌 유저의 경우 해당 기능은 사용할 수가 없습니다.
Food curation 서비스는 nginx와 pm2를 활용하여 aws에서 서비스 되고 있습니다.
ssl을 적용하여 프론트엔드와 백엔드 모두 https로 접근하도록 설정하였습니다.
프론트엔드와 백엔드 모두 pm2에서 관리되고 있으며 nginx에선 ssl을 위한 인증서 관리 및 프록시역할을 하고 있습니다.
파트: 개요, 지역별 경제 특성 분석, 상권 분석 및 추천, 체인점 분석, 업종별 경향 분석
-
개요
- 조사대상 → 다이닝 코드 이용자 및 ##은행 고객
- 조사지역 → 서울
- 활용 데이터: 다이닝코드데이터, 카드사 결제데이터, 통계청 데이터, 서울특별시 우리 마을 가게 데이터
- tf-idf로 분석한 최신 키워드
- 나이대별(2,3,4,50대) 소비 트렌드 → 은행 데이터(통계청 데이터 활용) + 나이별 소비 증가량 → 은행 데이터
-
지역별 경제 특성 분석 → 결제 데이터 활용
- 지역별 소비량 top3
- 성별 top1 소비량 지역
- 연령대별 지역 순위(소비량 top5 기준)
- 시간대별 지역 순위(소비량 top5 기준)
-
상권 분석 및 추천 → 결제 데이터 + 서울특별시 우리 마을 가게(상권분석서비스 가게 수 데이터 활용)
- 각 지역마다 소비량이 많은 상권 top3 보여주기
- 식생활과 관련된 상권을 추천해주기 위하여 각 카테고리마다 지역별 소비량을 가게 수로 나누어 매출이 좋은 지역을 보여줌
- 식생활과 관련된 상권: 한식, 중식, 양식, 일식/생선회집, 제과점/아이스크림점, 패스트푸드점, 일반주점, 커피/음료전문점으로 한정함
-
체인점 분석 → 다이닝 코드 데이터 활용
- 체인점 평점순으로 top 10
- 비체인/체인/전체 가게에 대한 평점 비교
-
업종별 경향 분석(스토캐스틱 분석) → 결제 데이터 활용
- 라이프 스타일 관련 업종: 의류, 악세사리류, 제과점/아이스크림점, 커피/음료전문점, 패스트푸드점, 한식, 일식/생선회집, 중식, 양식, 주점, 편의점, 숙박, 헬스장, 미용원/피부미용원, 화장품점으로 한정함
경로: api/algorithm_change
메소드: PUT
| 인자 | 필수 여부 |
|---|---|
| algorithm_list | True(0: "svdpp", 1: "knn") |
현재 서버에서 사용될 추천 알고리즘을 전송받은 데이터에 해당하는 알고리즘으로 변경합니다.
경로: api/algorithm_check
메소드: GET
현재 서버에서 어떤 추천 알고리즘이 적용되어 있는지 확인하는 API입니다.
경로: api/crawling_check
메소드: GET
지금 크롤링 해야 할 매장을 저장한 딕셔너리를 조회합니다.
경로: api/crawling_start
메소드: GET
크롤링 해야 할 매장이 저장된 딕셔너리를 조회하여 크로링을 시작하는 명령을 내립니다.
경로: api/create_store
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| store_name: String | True |
| branch: String | False |
| area: String | False |
| tel: String | False |
| address: String | False |
| latitude: Float | False |
| longitude: Float | False |
| category: String | False |
현재 사용자가 관리자일 경우 Json으로 인자를 담아서 보내면 해당 매장을 생성합니다.
경로: api/get_store_reviews_by_store_id/{store_id}
메소드: GET
| 인자 | 필수 여부 |
|---|---|
| store_id | True |
store_id를 입력받아 해당 매장의 리뷰 리스트를 반환합니다.
반환값
[
{
"id": INT,
"store": INT(store_id),
"store_name": String(store_name),
"user": INT(user_id),
"score": INT(score),
"content": String(content),
"reg_time": Datetime(reg_time),
"category_list": [
String(category),
]
}
]경로: api/like_store
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| store_id: INT | True |
| user_id: INT | True |
user_id와 store_id를 인자로 받아 해당하는 좋아요 객체가 있으면 그 객체를 지우고 없으면 객체를 생성합니다.
반환값
좋아요 객체 생성 시
"좋아요"좋아요 객체 삭제 시
"좋아요 취소"경로: api/recommend_by_current_location
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| latitude: Float | True |
| longitude: Float | True |
위치 정보를 입력받아 반경 10km 이내의 매장 중 추천할만한 매장을 반환합니다.
경로: api/recommend_by_store_id
메소드: GET
| 인자 | 필수 여부 |
|---|---|
| store_id: Int | True |
매장 아이디를 입력받아 반경 1km안에 있는 매장을 가져오고 각 매장별 카테고리 겹치는 횟수 및 인기도 점수를 활용하여 점수화하고 상위 매장을 반환합니다.
경로: api/relearning_current_model
메소드: GET
요청이 들어오면 현재 서버에 적용중인 알고리즘을 확인(svdpp or knn)하여 해당 모델을 다시 학습시킵니다.
경로: api/relearning_kmeans
메소드: GET
요청이 들어오면 현재 k-means 모델을 다시 학습시킵니다.(최신 유저 반영하여 다시 클러스터링 합니다.)
경로: api/reviews
메소드: GET
모든 리뷰의 store, user, score를 가져와 반환합니다.
반환값
[
{
"store": INT(store_id),
"user": INT(user_id),
"score": INT(score)
}
]경로: api/reviews_info
메소드: GET
모든 리뷰의 매장 아이디, 등록시간, 평점을 가져옵니다.
반환값
[
{
"store": INT(store_id),
"reg_time": datetime(user_id),
"score": INT(score)
}
]경로: api/search_store
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| latitude: Float | True |
| longitude: Float | True |
| words: String | False |
| dis: int(미터단위 정수) | True |
위치 정보와 검색어를 받아와서 반경 dis 미터 이내의 매장 중 검색어가 포함된 매장을 반환합니다.
반환값
[
{
"id": INT(pk),
"store_name": String(store_name),
"branch": String(branch),
"area": String(area),
"tel": String(phone_number),
"address": String(address),
"latitude": Float(latitude),
"longitude": Float(longitude),
"category_list": [
String(category)
],
"review_count": INT(review_count),
"menues": [
{
"id": INT(menu_id),
"store": INT(store_id),
"menu_name": String(menu_name),
"price": INT(price)
},
]
}
]경로: api/set_user_category
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| category: String | True |
카테고리를 인자로 받아 요청을 보낸 유저의 category에 반영합니다.
경로: api/store/{review_count}
메소드: GET
| 인자 | 필수 여부 |
|---|---|
| review_count: INT | True |
입력받은 리뷰갯수 이상인 매장들의 id, review_count를 받아 옵니다.
반환값
[
{
"id": INT(store_id),
"review_count": INT(review_count)
}
]경로: api/store_reviews
메소드: POST
인자를 입력하면 리뷰를 작성합니다.
| 인자 | 필수 여부 |
|---|---|
| store: INT(store_id) | True |
| user: INT(user_id) | True |
| score: INT | True |
| content: String | True |
반환값
"작성 성공"메소드: PUT
인자를 입력하면 리뷰를 수정합니다.
| 인자 | 필수 여부 |
|---|---|
| id: INT(review_id) | True |
| score: INT | True |
| content: String | True |
반환값
"수정 성공"메소드: DELETE
인자를 입력하면 리뷰를 삭제합니다.
| 인자 | 필수 여부 |
|---|---|
| id: INT(review_id) | True |
반환값
"삭제 성공"경로: api/stores
메소드: GET
| 인자 | 필수 여부 |
|---|---|
| page: INT | False |
| page_size: INT | False |
반환값
{
"count": INT(number of stores),
"next": String(url of next page),
"previous": String(url of next page),
"results": [
{
"id": INT(store_id),
"store_name": String(store_name),
"branch": String,
"area": String,
"tel": String(phone_number),
"address": String(address),
"latitude": Float(latitude),
"longitude": Float(longitude),
"category_list": [
String(category)
],
"review_count": INT,
"menues": [
{
"id": INT(menu_id),
"store": INT(store_id),
"menu_name": String(menu_name),
"price": INT(price)
}
],
"tag": String(tag),
"url": String(image url)
}
]
}매장 정보를 반환합니다.
메소드: PUT
| 인자 | 필수 여부 |
|---|---|
| pk: INT | True |
반환값
{
"count": INT(number of stores),
"next": String(url of next page),
"previous": String(url of next page),
"results": [
{
"id": INT(store_id),
"store_name": String(store_name),
"branch": String,
"area": String,
"tel": String(phone_number),
"address": String(address),
"latitude": Float(latitude),
"longitude": Float(longitude),
"category_list": [
String(category)
],
"review_count": INT,
"menues": [
{
"id": INT(menu_id),
"store": INT(store_id),
"menu_name": String(menu_name),
"price": INT(price)
}
],
"tag": String(tag),
"url": Stringmage url)
}
]
}메소드: DELETE
| 인자 | 필수 여부 |
|---|---|
| pk: INT | True |
반환값
삭제 성공입력받은 pk를 가지는 매장을 DB에서 제거합니다.
경로: api/update_learning_dataframe
메소드: GET
요청이 들어오면 학습시킬 데이터를 담아놓은 데이터프레임을 갱신합니다.(최신 DB를 반영하기 위함)
경로: api/user_based_cf
메소드: GET
요청이 들어오면 요청을 한 유저의 리뷰 갯수를 확인합니다.
작성한 리뷰 갯수가 10개 이상인 경우 현재 서버에 적용된 알고리즘을 확인(svdpp or knn)을 확인하고 서버에 적용된 알고리즘으로 계산하여 예측된 평점이 높은 순으로 추천할 매장을 반환합니다.
작성한 리뷰 갯수가 10개 미만인 경우 kmeans기반으로 현재 유저가 어떤 유저군에 속하는지 확인한 후 해당 군집에서 인기도 점수가 높은 매장을 추천합니다.
경로: api/user_reviews
메소드: GET
| 인자 | 필수 여부 |
|---|---|
| page: INT | False |
| page_size: INT | False |
반환값
{
"count": INT(number of users),
"next": String(url of next page),
"previous": String(url of next page),
"results": [
{
"id": INT(review_id),
"store": INT(store_id),
"store_name": String(store_name),
"user": INT(user_id),
"score": INT(user_id),
"content": String,
"reg_time": Datetime,
"category_list": [
String(category)
],
}
]
}현재 서버에서 적용된 협업필터링 기반 추천 알고리즘을 재학습시키는
관리자 페이지에서 모든 유저들을 보여주기 위한 api입니다.
staff 계정일 경우에만 사용이 가능합니다.
경로: api/all_user
메소드: GET
반환값:
- 성공일 경우: 활성화 상태인 모든 유저 데이터
- 실패일 경우: "접근 불가"
관리자 페이지에서 특정 유저의 권한을 변경해주기 위한 api입니다.
staff 계정일 경우에만 사용이 가능합니다.
경로: api/change_user
메소드: PUT
반환값:
- 성공일 경우: "권한 변경 성공"
- 실패일 경우: "접근 불가"
관리자 페이지에서 특정 유저를 비활성상태로 변경해주기 위한 api입니다.
staff 계정일 경우에만 사용이 가능합니다.
경로: api/delete_user
메소드: PUT
반환값:
- 성공일 경우: "삭제 성공"
- 실패일 경우: "삭제 실패"
체인점 평점 순위, 비체인/체인/전체 평점 비교 차트를 위한 데이터 전송 api입니다.
경로: api/compare_with_chain
메소드: GET
반환값: 체인점 평점 순위, 비체인/체인/전체 평점 데이터
나이대별, 시간대별 지역 소비량 순위 데이터 전송 api입니다.
경로: api/district_by_age_time
메소드: GET
반환값: 나이대별, 시간대별 지역 소비량 순위 데이터
연령대별 소비 트렌드 데이터 전송 api입니다.
경로: api/generation_consumption
메소드: GET
반환값: 연령대별 소비 트렌드 데이터(평균 사용량과 비율)
업종별 동향을 파악하기 위한 스토캐스틱 분석 결과 데이터 전송 api입니다.
경로: api/trend_by_tob
메소드: GET
반환값: 업종별 동향을 파악하기 위한 스토캐스틱 분석 결과 데이터
로그인 api입니다.
경로: api/login/
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| username: string | True |
| email: string | False |
| password: string | True |
반환값:
- token
- user정보(pk, username, email, gender, age, review_count, is_staff, category_list, status)
회원가입 api입니다.
이메일 인증 확인 후, 로그인이 가능합니다.
경로: api/rest-auth/registration/
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| username: string | True |
| email: string | True |
| password1: string | True |
| password1: string | True |
| age | False |
| gender | False |
반환값: 이메일이 전송되었습니다.
토큰을 검증하는 api입니다.
경로: api/token/verify/
메소드: POST
| 인자 | 필수 여부 |
|---|---|
| token: string | True |
반환값:
- 성공했을 경우: token
- 실패했을 경우: "Signature has expired."
회원탈퇴용 api입니다.
완전히 삭제하지 않고 비활성화시킵니다.
경로: api/user_withdrawal
메소드: POST
반환값:
- 성공했을 경우: "삭제 성공"
- 실패했을 경우: "삭제 실패"
