Skip to main content
Summary

AI 분석보다 먼저 정해야 할 것

리뷰 분석 서비스의 핵심은 AI 모델이 아니다. 사용자가 올린 리뷰가 어떤 매장에 속하는지, 한 번의 업로드가 어떤 분석 작업으로 이어지는지, 결과 리포트를 다시 열어볼 수 있는지가 먼저 정리되어야 한다.

AI API는 입력을 받아 답을 만든다. 하지만 서비스는 그 앞뒤를 책임져야 한다. 누가 올린 데이터인지, 어떤 리뷰를 분석했는지, 같은 리뷰가 반복 업로드되었는지, 분석이 실패했을 때 다시 시도할 수 있는지까지 관리해야 한다.

이번 장에서는 대시보드의 "리뷰 업로드하기" 버튼 이후를 만든다. 사용자는 리뷰를 붙여넣거나 CSV 파일로 올린다. 서비스는 데이터를 검증하고 저장한다. 그다음 분석 작업을 만들고, 다음 장에서 AI 분석 엔진이 이 작업을 처리한다.

데이터 모델과 업로드 흐름 설계AI 생성 이미지

첫 버전의 데이터 관계

첫 버전에서 우리가 직접 만들 핵심 테이블은 다섯 개다.

  1. 매장

  2. 리뷰 묶음

  3. 개별 리뷰

  4. 분석 작업

  5. 리포트

사용자 정보는 별도로 생각한다. Supabase Auth를 쓰면 회원 정보는 auth.users에 저장된다. 우리가 직접 만드는 테이블은 이 사용자 ID를 기준으로 연결한다. 모든 주요 테이블에는 user_id를 둔다. 그래야 사용자별 데이터 격리를 단순하게 만들 수 있다.

구조는 다음처럼 생각하면 된다.

user
 └─ store
     └─ review_batch
         ├─ review
         └─ analysis_job
              └─ report

store는 음식점이나 카페 한 곳을 뜻한다. 한 사용자가 여러 매장을 운영할 가능성은 있지만, 첫 버전에서는 한 매장만 있어도 된다. 그래도 테이블은 여러 매장을 담을 수 있게 만든다. 나중에 확장할 때 구조를 갈아엎지 않기 위해서다.

review_batch는 한 번 업로드한 리뷰 묶음이다. 예를 들어 "2026년 7월 네이버 리뷰 82개"가 하나의 묶음이다. 사용자는 같은 매장에 대해 여러 번 리뷰를 올릴 수 있다. 이 묶음 단위가 있어야 과거 분석과 새 분석을 구분할 수 있다.

review는 실제 리뷰 한 건이다. 별점, 작성일, 작성자 표시명, 본문, 출처 같은 값을 담는다. 모든 리뷰 데이터가 항상 깔끔하게 들어오지는 않는다. 어떤 CSV에는 별점이 없고, 어떤 리뷰는 날짜가 빠져 있다. 그래서 필수 값과 선택 값을 나눠야 한다.

analysis_job은 AI 분석을 요청한 작업 기록이다. 업로드가 끝났다고 바로 리포트가 생기는 것은 아니다. 분석 요청이 대기 중인지, 처리 중인지, 실패했는지, 완료되었는지를 저장해야 한다.

report는 사용자가 보는 최종 결과다. 반복 불만, 칭찬 포인트, 개선 액션, 답글 초안, 마케팅 문구가 여기에 저장된다. AI가 같은 데이터를 다시 분석할 때마다 결과가 달라질 수 있으므로, 생성된 리포트는 별도로 저장해두는 편이 좋다.

테이블을 작게 나누는 이유

초기 개발에서는 "그냥 reviews 테이블 하나에 다 넣으면 되지 않을까?"라는 생각이 든다. 하지만 곧 문제가 생긴다.

리뷰 100개를 올렸는데 20개가 중복이면 어떻게 표시할 것인가. 분석이 실패했을 때 리뷰를 다시 올리게 할 것인가. 한 매장에 대해 6월 리뷰와 7월 리뷰를 비교하고 싶다면 어디서 구분할 것인가.

그래서 최소한의 분리가 필요하다.

stores
- id
- user_id
- name
- business_type
- location
- main_review_source
- created_at

review_batches
- id
- user_id
- store_id
- source
- upload_method
- original_file_path
- review_count
- valid_review_count
- duplicate_count
- status
- created_at

reviews
- id
- user_id
- store_id
- batch_id
- source
- external_review_id
- author_name
- rating
- review_date
- content
- content_hash
- metadata
- created_at

analysis_jobs
- id
- user_id
- store_id
- batch_id
- status
- error_message
- started_at
- finished_at
- created_at

reports
- id
- user_id
- store_id
- batch_id
- analysis_job_id
- summary
- complaint_patterns
- praise_patterns
- action_items
- reply_templates
- marketing_copy
- share_token
- is_public
- created_at

여기서 metadata는 JSON 형태로 둔다. CSV마다 열 이름이 다르고, 리뷰 출처마다 부가 정보가 다르기 때문이다. 처음부터 모든 컬럼을 정교하게 만들려고 하면 업로드 흐름이 늦어진다. 핵심 분석에 필요한 값은 컬럼으로 빼고, 나머지는 metadata에 담는다.

content_hash는 중복 감지를 위해 둔다. 리뷰 본문에서 앞뒤 공백을 제거하고, 연속된 공백을 하나로 줄인 뒤, 소문자화 같은 정규화를 거쳐 해시를 만든다. 같은 매장, 같은 출처, 같은 본문 해시가 이미 있으면 중복으로 처리한다. 출처에서 제공하는 고유 리뷰 ID가 있다면 external_review_id도 함께 저장해두면 나중에 더 정확하게 중복을 판단할 수 있다.

MVP의 리뷰 수집 방식 선택

리뷰를 가져오는 방법은 크게 세 가지다.

첫째, 사용자가 리뷰를 직접 붙여넣는 방식이다. 개발이 가장 단순하고, 외부 플랫폼 연동이 필요 없다. 사용자는 리뷰 목록을 복사해 입력창에 붙여넣는다. 대신 날짜, 별점, 작성자를 구조화하기 어렵다.

둘째, CSV 파일 업로드 방식이다. 엑셀이나 스프레드시트에 정리한 데이터를 올릴 수 있다. 별점, 날짜, 본문을 열 단위로 받을 수 있어 분석 품질이 좋아진다. 대신 파일 파싱, 열 매핑, 파일 보안 처리가 필요하다.

셋째, 리뷰를 한 건씩 수동 입력하는 방식이다. 데이터 품질은 높지만 리뷰가 30개만 넘어도 사용자가 지친다. 우리 서비스의 약속은 시간을 줄여주는 것이므로, 수동 입력을 기본 방식으로 삼으면 약속과 충돌한다.

첫 MVP에서는 "붙여넣기 우선, CSV 업로드 보조"가 현실적이다.

랜딩페이지에서 무료 진단을 신청한 사장님은 대부분 완성된 CSV를 갖고 있지 않을 가능성이 높다. 따라서 큰 입력창을 제공하고, 예시 형식을 보여준다.

2026-07-01 / 5점 / 음식이 빨리 나오고 친절했어요.
2026-07-03 / 3점 / 맛은 좋은데 대기 시간이 길었습니다.
2026-07-05 / 2점 / 포장이 식어서 왔어요.

동시에 CSV 업로드 버튼도 둔다. 이미 리뷰를 정리해둔 사용자나 운영 대행사 고객을 놓치지 않기 위해서다. 다만 첫 화면에서 세 가지 방식을 모두 같은 비중으로 보여주지는 않는다. 선택지가 많으면 사용자가 멈춘다.

권장 화면 순서는 다음과 같다.

  1. 리뷰 붙여넣기 입력창

  2. "CSV 파일이 있다면 업로드" 보조 버튼

  3. 샘플 데이터로 체험하기 링크

샘플 데이터는 중요하다. 사용자가 실제 리뷰를 넣기 전에 결과를 보고 싶어 하기 때문이다. 샘플 분석은 전환을 돕지만, 실제 분석과 구분해야 한다. 샘플 리포트에는 결제나 실제 운영 판단을 과하게 유도하지 않는다.

업로드 화면의 흐름

업로드는 한 번에 끝나는 작업이 아니다. 사용자가 데이터를 넣고, 서비스가 검증하고, 사용자가 확인한 뒤, 분석을 요청하는 흐름이 필요하다.

첫 버전의 흐름은 네 단계면 충분하다.

입력 방식 선택
→ 리뷰 데이터 입력
→ 미리보기와 오류 확인
→ 분석 요청 생성

입력 단계에서는 매장과 리뷰 출처를 함께 받는다. 이미 온보딩에서 매장 정보를 입력했다면 기본값으로 채운다.

매장: 연남동 작은카페
리뷰 출처: 네이버 플레이스
업로드 방식: 붙여넣기
분석 대상 기간: 최근 3개월

사용자가 리뷰를 붙여넣으면 서버에서 파싱한다. 클라이언트에서만 검증하면 사용자가 우회할 수 있다. 브라우저에서는 빠른 미리보기만 제공하고, 최종 검증은 서버 액션이나 API 라우트에서 처리한다.

검증이 끝나면 미리보기 화면을 보여준다.

총 82개 리뷰를 찾았습니다.
- 분석 가능: 76개
- 중복 의심: 4개
- 본문 없음: 2개

여기서 바로 AI 분석을 시작하지 않는다. 사용자가 "이 데이터로 분석하기"를 눌러야 analysis_jobs에 작업을 만든다. 이 한 단계가 있어야 비용을 통제할 수 있다. AI 분석은 호출할 때마다 비용이 발생하므로, 잘못 붙여넣은 데이터까지 자동으로 분석하면 손실이 생긴다.

검증 규칙은 친절해야 한다

데이터 검증은 사용자를 혼내는 기능이 아니다. 좋은 결과를 만들기 위해 필요한 최소 안내다. 오류 메시지는 기술적인 말보다 수정 방법을 알려줘야 한다.

첫 버전에서 필요한 검증 규칙은 다음과 같다.

항목규칙사용자에게 보여줄 말
빈 본문리뷰 내용이 없으면 제외"내용이 없는 리뷰 2개는 분석에서 제외됩니다."
너무 짧은 리뷰5자 미만은 제외 또는 낮은 가중치"짧은 리뷰는 전체 경향 분석에만 약하게 반영됩니다."
너무 긴 리뷰예: 2,000자 초과 시 잘라 저장"매우 긴 리뷰는 앞부분 중심으로 분석됩니다."
중복 리뷰같은 매장·출처·본문 해시 기준으로 감지"이미 등록된 것으로 보이는 리뷰 4개를 제외했습니다."
별점 오류1~5 범위를 벗어나면 비움 처리"별점 형식이 맞지 않는 항목은 별점 없이 분석됩니다."
날짜 오류날짜 파싱 실패 시 비움 처리"날짜를 알 수 없는 리뷰는 기간 분석에서 제외됩니다."
개인정보전화번호, 이메일 등 감지 시 마스킹"개인정보로 보이는 내용은 일부 가린 뒤 저장합니다."

개인정보 처리는 특히 중요하다. 리뷰에는 고객 이름, 전화번호, 예약 정보, 직원 이름이 섞여 들어갈 수 있다. 첫 버전에서는 완벽한 개인정보 탐지를 약속하지 말고, 기본적인 패턴을 마스킹한다.

예를 들어 이메일은 *@* 형태로, 전화번호는 010-- 형태로 바꾼다. 그리고 업로드 화면 하단에 "민감한 개인정보는 업로드 전에 삭제해 주세요"라는 안내를 둔다. 기술적 보호와 사용자 안내를 함께 써야 한다.

원본 파일을 저장할지 결정하기

CSV 업로드를 지원하면 원본 파일을 어디에 둘지도 정해야 한다. Supabase Storage를 사용할 수 있지만, 원본 파일 저장은 신중해야 한다. Storage 접근도 데이터베이스와 마찬가지로 권한 정책이 필요하며, 기본적으로 허용 정책을 만들기 전에는 업로드나 읽기를 할 수 없다.1

MVP에서는 다음 기준을 추천한다.

  • 붙여넣기 입력: 원본 텍스트 전체를 별도 저장하지 않고 파싱된 리뷰만 저장

  • CSV 업로드: 비공개 버킷에 원본 파일 저장 후 review_batches.original_file_path에 경로 기록

  • 분석 완료 후 일정 기간이 지나면 원본 파일 삭제 가능

원본을 저장하면 사용자가 문제를 제기했을 때 재처리할 수 있다. 반면 개인정보와 보안 부담이 커진다. 1인 창업가에게는 무조건 많이 저장하는 것보다, 다시 분석할 수 있을 만큼만 저장하는 편이 낫다.

파일 경로는 사용자 ID를 포함해 나눈다.

review-uploads/{user_id}/{batch_id}/reviews.csv

이 구조를 쓰면 Storage 정책에서도 사용자별 폴더 접근을 제한하기 쉽다. 단, service_role 키로 모든 파일을 처리하는 코드는 서버에서만 사용해야 한다. 브라우저 코드에 노출되면 다른 사용자의 파일까지 접근할 수 있는 사고로 이어질 수 있다.

RLS로 사용자별 데이터 격리하기

SaaS에서 가장 위험한 실수는 A 사용자가 B 사용자의 리포트를 보는 것이다. 디자인이 조금 어색한 것은 고칠 수 있지만, 데이터 노출은 신뢰를 무너뜨린다.

Supabase에서는 테이블마다 Row Level Security, 줄여서 RLS를 켤 수 있다. RLS는 행 단위 권한 제어다. RLS를 켠 테이블은 정책이 없으면 공개 키로 접근하는 API에서 기본적으로 데이터 접근이 차단된다. 정책을 만들 때는 auth.uid()로 현재 로그인한 사용자의 ID를 확인하고, USING과 WITH CHECK를 구분해 사용한다.2

기본 패턴은 단순하다.

alter table public.stores enable row level security;

create policy "Users can read own stores"
on public.stores
for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Users can insert own stores"
on public.stores
for insert
to authenticated
with check ((select auth.uid()) = user_id);

create policy "Users can update own stores"
on public.stores
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

create policy "Users can delete own stores"
on public.stores
for delete
to authenticated
using ((select auth.uid()) = user_id);

같은 패턴을 review_batches, reviews, analysis_jobs, reports에도 적용한다. 핵심은 모든 테이블에 user_id가 있어야 한다는 점이다. 외래 키를 따라가며 복잡하게 권한을 확인할 수도 있지만, 첫 버전에서는 각 테이블에 소유자를 명시하는 방식이 이해하기 쉽고 실수도 적다.

그리고 user_id에는 인덱스를 추가한다.

create index idx_stores_user_id on public.stores(user_id);
create index idx_reviews_user_id on public.reviews(user_id);
create index idx_reports_user_id on public.reports(user_id);

권한 정책은 매 요청마다 검사된다. 정책에 자주 쓰는 컬럼에 인덱스가 없으면 데이터가 늘어날수록 느려진다. 실제 구현에서는 store_id, batch_id, analysis_job_id처럼 자주 조회하고 연결하는 컬럼에도 인덱스를 추가해두면 좋다.

공유 리포트는 별도 규칙으로 다룬다

나중에 리포트 공유 기능을 만들면 문제가 하나 더 생긴다. 사용자가 "고객이나 직원에게 이 리포트를 링크로 보여주고 싶다"고 할 수 있다. 이때 reports를 완전히 공개하면 안 된다.

첫 설계에서는 share_token과 is_public을 둔다.

/reports/share/{share_token}

공유 페이지는 로그인 없이 볼 수 있지만, is_public = true이고 토큰이 일치하는 리포트만 조회한다. 이 조회는 일반 사용자 대시보드 조회와 분리한다. 대시보드에서는 여전히 user_id = auth.uid() 정책을 적용한다.

공유용 리포트에는 원본 리뷰 전체를 보여주지 않는 편이 안전하다. 요약, 개선 액션, 마케팅 문구처럼 가공된 결과만 보여준다. 원본 리뷰에는 개인정보나 민감한 불만이 포함될 수 있기 때문이다.

이번 장의 구현 기준

이번 장에서 만들 범위는 명확하다.

  • stores, review_batches, reviews, analysis_jobs, reports 테이블을 만든다.

  • 모든 주요 테이블에 user_id를 둔다.

  • RLS를 켜고, 로그인한 사용자가 자기 데이터만 읽고 쓸 수 있게 한다.

  • 리뷰 붙여넣기를 기본 업로드 방식으로 만든다.

  • CSV 업로드는 보조 방식으로 두되, 원본 파일은 비공개 Storage에 저장한다.

  • 업로드 후 바로 AI를 호출하지 않고, 미리보기와 확인 단계를 거친다.

  • 중복, 빈 값, 긴 텍스트, 개인정보를 기본 검증 규칙으로 처리한다.

이제 서비스는 사용자의 입력을 받을 준비를 갖췄다. 다음 장에서는 analysis_jobs에 쌓인 작업을 가져와 AI API에 보내고, 사장님이 바로 읽을 수 있는 리포트 구조로 바꾸는 과정을 만든다.

참고

1Supabase Storage Access Control

2Supabase Row Level Security

책 목록

혼자서 다 하는 창업: 1인 창업가용 AI 풀스택

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기