← 프로젝트

음식점 추천여행 · 로컬 발견

Locals Only

로컬이 실제로 가는 집을, 메뉴에서 시작해 찾아주는 맛집 발견 서비스 — 랭킹 · 메뉴 파이프라인 · 큐레이션 어드민까지 1인 개발

서비스 써보기 ↗

기간
2022 – 운영 중
역할
1인 프로젝트 — 기획 · 데이터 파이프라인 · 랭킹 모델 · 웹앱 · 어드민
상태
웹 서비스 운영 중 (한국어 · 영어 · 일본어)
14.7만
가게
187만
메뉴 행 → 대표메뉴로 정규화
상위 5%
권역 × 업종별 로컬 픽 배지
51.6% → 73.6%
대표메뉴 매핑 정확도 (규칙 → 규칙+LLM)

한 줄로

평균 별점 4.3은 아무것도 말해주지 않습니다. 늘 5점을 주는 사람의 5점과 평소 3점을 주던 사람의 5점은 다른 신호입니다. 이 차이를 점수로 만든 랭킹 위에, “오늘 뭐 먹지?”에서 출발해 메뉴 → 그 메뉴를 잘하는 로컬 가게로 좁혀 가는 서비스를 만들어 운영하고 있습니다. 모델만이 아니라 데이터 파이프라인, 웹앱, 그리고 사람이 데이터를 직접 고치는 어드민까지 혼자 만들었습니다.

문제 정의 — 두 개의 모드

  • 토박이 모드 — 내 생활반경 안에서 “요즘 마라탕 땡기는데 여기서 제일 잘하는 집”처럼, 카테고리를 하나씩 격파하고 싶은 니즈. 검색 결과 나열이 아니라 적절히 추려진 리스트가 핵심입니다.
  • 여행자 모드 — 낯선 곳에서 광고에 오염되지 않은 진짜 로컬 맛집을 찾는 니즈. 서울뿐 아니라 경기·제주·강원 데이터를 함께 다루고, 앱은 한국어·영어·일본어를 지원합니다. 한국어가 아닐 때는 음식 카드에서 바로 가게로 넘기지 않고 음식 설명을 먼저 보여줍니다.

1. 랭킹 — 리뷰 신뢰도 기반 Bayesian 점수

학습 기반 모델이 아니라, 리뷰 단위 신호를 점수화한 뒤 식당 단위로 집계하고 Bayesian 보정을 거치는 해석 가능한 랭킹입니다.

  1. 리뷰어 ‘당시’ 평균 역산 — 현재 평균은 미래 정보를 포함합니다. 리뷰를 시간순으로 정렬해 직전까지의 누적 평균을 구하고 별점 − 당시 평균으로 편차를 계산해 시점 리키지를 피했습니다.
  2. 리뷰 단위 4요소 — 리뷰어 등급 25% · 최신성 15%(half-life 730일) · 평점 편차 40% · 신뢰·반응 20%
  3. Bayesian 보정 — 리뷰 20개 분량의 prior를 동일 카테고리 평균으로 걸어, 리뷰 몇 개로 우연히 고득점을 받는 신규 식당의 왜곡을 줄였습니다.

2. 로컬 픽 배지 — 점수를 그대로 보여주지 않은 이유

실데이터(리뷰가 있는 112,216곳)에서 Bayesian 점수 분포가 p10=26 · p50=31 · p90=34로 좁게 몰려 있었습니다. “32점”을 그대로 노출하면 실제로는 상위권인데 100점 만점의 낙제점으로 읽히고, 상위 15%와 하위 33%의 차이가 3점뿐이라 비교도 안 됩니다. 그래서 숫자 대신 상위 5% 통과 여부만 배지로 보여줍니다.

  • 전국 단일 컷은 ‘서울 배지’가 된다 — 리뷰 20개 이상 가게에 전국 컷을 적용하면 서울 1,408곳 통과, 제주 1곳, 강원 0곳. 강원은 최고점이 전국 컷보다 낮아 통과 자체가 불가능했습니다.
  • 업종도 갈라야 한다 — 같은 서울 안에서도 간식 0.3642 · 분식 0.3439로 선이 달라, 권역 컷 하나만 쓰면 간식·카페류가 배지를 과점합니다.
  • 그래서 컷을 권역 × 대분류 셀 단위로 잡고, 표본 100곳 미만 셀은 권역 컷 → 전국 컷으로 단계적으로 떨어지게 했습니다. 리뷰 20개 이상 27,608곳 중 97.3%가 셀 컷을 받습니다.

3. 메뉴 파이프라인 — 187만 개의 메뉴 이름을 ‘대표메뉴’로

메뉴에서 시작하는 탐색을 하려면 “원조 충무김밥”, “충무김밥(2인)”, “충무 김밥”이 같은 음식이라는 걸 알아야 합니다.

  • 규모 — 가게 146,858곳, 메뉴 행 1,869,603개, 정규화 후에도 고유 메뉴명 263,724개(72.4%가 한 가게에만 등장하는 롱테일)
  • 음식/비음식 분류 — 음료·주류·사이드·요금 등 6종 사전 기반 분류기, test 정확도 97.9%(비음식 precision 100%)
  • 대표메뉴 매핑 — 규칙 베이스라인 51.6% → 68.1%, 규칙 + 로컬 LLM 하이브리드로 73.6%. 740건 gold set으로 매 변경을 판정하고, 이 과정에서 “제안 규칙의 우선순위가 거꾸로였다”는 걸 실측으로 발견해 순서를 바꿨습니다.
  • 연산은 로컬, 발행만 Supabase — 임베딩·점수·후보 큐 같은 중간 산출물은 DuckDB/Parquet로 로컬에 두고, 앱이 실제로 읽는 테이블만 발행합니다.

4. 큐레이션 어드민 — 파이프라인이 틀린 곳을 사람이 고친다

매핑 정확도 73.6%는 곧 26%는 사람이 봐야 한다는 뜻입니다. 그래서 운영자가 직접 데이터를 고치는 어드민(/admin)을 따로 만들었습니다.

  • 대표메뉴 관리 — 이름 · 설명 편집, 후보 갤러리에서 대표 이미지 고르기, 노출 중지 / 통합 / 삭제
  • 소속 메뉴 정리 — 대표메뉴에 묶인 원본 메뉴를 이름별로 집계해(연결 수 순) 30개씩 검토하고, 여러 이름을 체크해 한 번에 다른 대표메뉴로 이동하거나 새 대표메뉴로 승격. 한글 NFC/NFD를 함께 처리하는 부분 검색, 한 번에 최대 1,000개 이름 처리
  • 원본 메뉴 탐색 — 전체 원본 메뉴에서 아직 대표메뉴가 없는 음식을 찾아 신규 등록
  • 가게별 조정 — 가게마다 대표메뉴 개수 · 순서 · 제외 · 수동 추가
  • 인기 픽 큐레이션 — 픽 편집과 노출 순서 정렬

어드민을 만들면서 가장 신경 쓴 건 “사람이 고친 것은 재발행이 덮지 않는다” 는 원칙입니다.

  • 파이프라인을 다시 돌려 테이블을 통째로 재발행(TRUNCATE → COPY)해도, 잠금 플래그 · 별도 override 테이블 · 트리거로 수동 큐레이션이 자동 재적용됩니다.
  • 일괄 변경은 적용 전에 실제 영향 수를 미리보기하고, 그 사이 데이터가 바뀌면 재확인을 요구합니다. 100개가 넘는 묶음도 한 트랜잭션으로 처리합니다.
  • 삭제는 상태만 바꾸는 soft delete로 두고, 숨김·삭제·통합된 항목은 restrictive RLS로 공개 API에서 빠지게 했습니다. 쓰기는 서버 라우트에서만 service role 키로 수행해 브라우저에 키가 노출되지 않습니다.
  • 빈 임시 DB에서 도는 SQL 테스트로 페이지 무관 집계, 부분 변경 방지, 동시 변경 감지, 재발행 후 보존까지 검증합니다.

4년짜리 반복

2022년 ‘평균 평점 3.5 이상’이라는 내 맛집 찾기 습관을 규칙으로 옮긴 kakaoRok, ‘유의미하게 만족한 리뷰어 수’로 목록을 추린 Streamlit 기반 What2Eat(머먹), 랭킹 모델을 정립한 TasteRoute, 그리고 메뉴에서 시작하는 지금의 Locals Only까지 — “좋은 식당이란 무엇인가”라는 같은 질문을 4년째 다듬고 있습니다.

한계와 다음 단계

  • 현재 랭킹은 개인화가 아닙니다. “모두에게 신뢰할 만한 가게”를 고르는 랭킹이고, 개인화는 쩝쩝LAB 리서치의 추천 레이어를 얹는 다음 단계입니다. 랭킹 점수는 그때 prior로 쓸 계획입니다.
  • 대표메뉴 매핑은 73.6%로, 나머지는 어드민 큐레이션에 기대고 있습니다. gold set precision 0.99 이상을 만족하는 임계로 별칭 사전을 자동 승격하는 것이 다음 과제입니다.
  • 사용자 행동(클릭·즐겨찾기) 기반으로 랭킹을 검증하는 온라인 지표는 아직 없습니다.

비상업적 개인 연구·학습 프로젝트입니다. 지도 서비스의 공개 리뷰 데이터를 연구 목적으로 수집해 사용하며, 수익화 계획은 없습니다.