간이 회원(lite) 후기 + 매장 QR 후기 직행 + 비로그인 시설 조회 - #132
Conversation
QR·공유 링크로 들어온 손님이 정식 가입 없이 전화번호 인증만으로 후기를 남기게
한다. 진입점은 후기 작성 플로우 하나뿐이다(signup-lite 가 purpose='review' 인증만
받아들이므로 로그인 화면 등 다른 경로로는 계정을 만들 수 없다).
설계의 핵심 둘:
1) **계정은 처음부터 하나다 — 병합이 아니라 승격.**
users.phone 에 유니크 인덱스가 있어 같은 번호는 같은 행일 수밖에 없다. 간이로
쓴 뒤 정식 가입하면 새 행을 만드는 게 아니라 그 행에 username/password_hash/
nickname 을 채운다. 덕분에 facility_reviews_of 의 visit_no(user_id 파티션
row_number)가 끊기지 않고 "3번째 방문"으로 이어진다. 반대로 승격을 넣지 않으면
signup_user 가 phone_taken 으로 **정식 가입 자체를 막는다**.
2) **비회원 취급 — 기본 전면 차단, 후기만 예외.**
app.uid() 가 status='active' 를 요구하므로 status='lite' 로 두면 RLS·RPC 가
전부 자동으로 막힌다(채팅·게시글·펫 등 어디도 손댈 필요 없음). 후기 작성 하나만
app.uid_lite() 로 뚫는다. 새 기능이 생겨도 기본값이 차단이라 안전하다.
매 작성마다 재인증: add_facility_review 가 lite 계정이면 최근 15분 내 review 인증을
요구한다. 클라이언트만 막으면 우회되므로 서버에서 검사한다.
· app.mask_phone — 표시명 `***-1***-**78`. 조합이 1,000가지뿐이라 nickname
(대소문자 무시 유니크)에 넣으면 즉시 충돌한다. nickname 에는 비노출 내부값을
넣고 조회 RPC 가 번호에서 마스크를 만들어 돌려준다.
· signup-lite — 코드 검증·계정 확보·단명 토큰(15분)을 한 호출에 묶는다. 검증과
발급을 나누면 남이 받은 인증으로 세션을 가로챌 창이 생긴다(정식 signup 은
비밀번호가 한 겹 더 있어 그 창이 없다).
· purpose='review' 신설 — send-phone-code 가 signup 목적은 이미 가입된 번호에
발송을 거부하는데, 간이 후기는 정식 회원도 쓸 수 있어 그 차단에 걸리면 안 된다.
phone_verifications_purpose_check 제약도 함께 수정(안 하면 INSERT 가 막힌다).
· facilities_search 를 anon 에 개방 — 시설은 '주인 없는 프로필' 이라 손님이
상호로 찾아 후기까지 갈 수 있어야 한다.
· share_view_load 의 facility_preview 에 id/owner_user_id 추가 — QR 302 대상.
프로덕션 검증(전부 rollback): 마스크 형식, 동의·인증 거부, 같은 번호 재사용,
app.uid() 차단 대 uid_lite() 허용, visit_no 1→2, 인증 만료 시 reverify_required,
승격 후 표시명 전환과 회차 유지, 정식 계정 중복 가입 거부 — 13개 항목 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
51cefc5 의 사람/크롤러 분기 위에 facility_preview 분기를 얹는다. 손님이 매장에서 QR 을 찍는 목적은 후기다. 미리보기를 한 번 거치게 하면 그만큼 이탈하므로 바로 작성 화면을 연다(작성 화면 상단에 매장 이름이 나온다). 대상이 업체 계정이 아니라 **시설(facility)** 인 게 중요하다. QR 을 나눠줄 매장 대부분은 아직 업체 인증 전이라 프로필이 없지만, 시설 행은 공공데이터로 이미 존재하고(전국 24,552곳) 후기도 시설에 달린다. 나중에 그 매장이 인증하면 business_profiles.matched_facility_id 로 같은 시설에 붙는다 — QR 을 먼저 뿌려도 주인이 나중에 나타나는 순서가 성립한다. 크롤러에게는 게시글과 같은 규칙으로 서버 렌더링을 그대로 준다(링크 미리보기 유지). 프로덕션 확인: 사람 UA → 302 /r/<facilityId>, facebookexternalhit → og:title "포메이트 테스트샵" / og:description "★4.2 · 후기 5개 · 미용" 정상. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
20260718065841 하드닝이 이 함수들에서 anon 실행권한을 회수하며 근거를 이렇게 적었다 — "앱은 비로그인 상태에서 check_username_available 만 호출하므로 그 외 anon 불필요". **그 전제가 0029 로 깨졌다.** 매장 QR 로 들어온 손님은 로그인 전에 매장을 찾고 후기를 읽고 남기기까지 한다. 특히 지도는 게스트 모드에서 열리는데 facilities_within 이 42501 로 막혀, 마커가 하나도 없이 "주변 시설을 불러오지 못했어요" 라는 **원인과 무관한 오류**만 떴다 (실제 사용자 신고로 발견). 마커만 열면 마커를 눌렀을 때 상세·후기에서 같은 오류를 다시 만나므로 읽기 전용 4종을 함께 연다. facilities_within · facility_all_categories · facility_reviews_of · facility_review_by_id 안전성: 전부 읽기 전용이고 드러나는 데이터는 이미 anon 에 공개돼 있다. facilities 는 RLS `using (true)` + anon SELECT, 후기는 visibility_status='visible' 만 반환, owner_user_id 는 public_profiles.business_facility_id 로 이미 조회 가능하다. 같은 데이터를 RPC 로도 읽게 하는 것뿐이다. 열지 않은 것(의도적): ensure_naver_facility 는 행을 만드는 쓰기다(게스트가 후기를 쓸 땐 간이 인증으로 이미 authenticated 토큰을 들고 있어 열 필요가 없다). posts_by_region·feed_region_codes 는 지도의 게시글 레이어로 별개 관심사다.⚠️ Supabase 린터의 anon_security_definer_function_executable 경고가 함수 수만큼 다시 뜬다. 위 근거대로 의도된 것이니 advisor 정리 때 도로 잠그지 말 것. 스키마 스냅샷은 20260727100000·20260727170000 두 마이그레이션을 함께 반영한다 (안 하면 CI 가 구스키마로 돈다). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0029.md — 간이 회원 설계. 왜 이렇게 됐는지가 코드만 봐서는 안 보이는 결정들을 남긴다.
· status='lite' 를 고른 이유 — app.uid() 가 active 를 요구하므로 기본이 차단이
된다. 화이트리스트 방식이었다면 새 RPC 마다 빠뜨릴 위험이 생긴다.
· 승격이 선택이 아닌 이유 — users_phone_uq 때문에 승격을 넣지 않으면 간이로
후기를 쓴 사람이 정식 가입을 아예 못 한다.
· 마스크를 nickname 컬럼에 넣으면 안 되는 이유 — 조합 1,000가지 대 유니크 제약.
· QR 대상이 업체 계정이 아니라 시설인 이유 — 시설이 곧 '주인 없는 프로필'이고,
인증 전 매장에 뿌린 QR 도 나중에 주인이 나타나는 순서가 성립한다.
· business_profiles 에 주인 없는 행을 미리 만들지 말라는 경고.
0028 §배경이 적은 "스캔 → 설치 → 전화번호 인증 → 위치 동의 → 지역 인증 → 후기
작성" 퍼널이 **사실과 달랐다는 것**도 기록했다. add_facility_review 에는 지역·위치
조건이 없고 트리거도 전부 AFTER 다. 실제 마찰은 설치와 가입 둘뿐이었다.
docs/supabase-api.md — signup-lite 항목 신설(verify-phone-code 를 따로 부르지 않는
이유, 토큰이 15분인 이유, 표시명 규칙이 3곳에 중복된다는 경고 포함), 전화 인증
review 목적, §3.2-1 간이 회원 흐름, signup 의 lite 승격 분기.
문서가 2026-07-02 스냅샷이라 실제 배포(21개)와 벌어져 있던 것도 드러났다. 이번에
확인한 범위만 갱신하고, 항목이 없는 4개(apply-business·check-business-no·
purge-business-docs·share-view)를 표로 명시했다 — 읽지 않은 코드를 지어내지 않기
위해 용도와 설계 문서만 적었다.
README — db push 금지 경고. 이력 테이블이 실제와 어긋나 이미 적용된 6개를
재실행하려 든다(이번 배포에서 확인). psql 직접 적용과 rollback 사전 검증을 적었다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
추가: 설계 문서 + API 레퍼런스 (
|
왜
매장에 QR 을 나눠주고 후기를 부탁하는 동선을 서버에서 받친다. 손님이 QR 을 찍고 후기를 남기기까지 가입 절차 없이 갈 수 있어야 한다.
무엇
1. 간이 회원(lite) — 후기 작성 전용 비회원 계정
users.status='lite'로 구현했다.app.uid()가status='active'를 요구하므로 lite 는 RLS·RPC 에서 자동으로 전부 막히고, 후기 작성 하나만app.uid_lite()로 뚫는다. 새 기능이 생겨도 기본값이 차단이라 빠뜨릴 위험이 없다.계정은 처음부터 하나다 — 병합이 아니라 승격.
users.phone이 유니크라 같은 번호는 같은 행일 수밖에 없다. 간이로 쓴 뒤 정식 가입하면 그 행에 username/password_hash/nickname 을 채운다. 덕분에 방문 회차(visit_no)가 끊기지 않는다. 승격을 넣지 않으면signup_user가phone_taken으로 정식 가입 자체를 막는다.매 작성마다 재인증을 서버에서 강제한다 —
add_facility_review가 lite 계정이면 최근 15분 내purpose='review'인증을 요구한다.표시명은
***-1***-**78. 조합이 1,000가지뿐이라nickname(대소문자 무시 유니크)에 넣으면 충돌하므로, nickname 에는 비노출 내부값을 두고 조회 RPC 가 번호에서 마스크를 만든다.2. 매장 QR → 후기 작성 화면 302
대상이 업체 계정이 아니라 시설(facility) 이다. QR 을 뿌릴 매장 대부분은 아직 업체 인증 전이라 프로필이 없지만 시설 행은 공공데이터로 이미 있다(전국 24,552곳). 나중에 인증하면
matched_facility_id로 같은 시설에 붙는다.3. 비로그인 시설 조회 개방
20260718065841하드닝이 "앱은 비로그인 상태에서 check_username_available 만 호출한다" 는 전제로 잠근 함수들인데, 그 전제가 이번 변경으로 깨졌다. 지도는 게스트 모드에서 열리는데facilities_within이 42501 로 막혀 "주변 시설을 불러오지 못했어요" 라는 원인과 무관한 오류만 떴다(실사용 신고로 발견).전부 읽기 전용이고 드러나는 데이터는 이미 anon 에 공개돼 있다(
facilities는 RLSusing (true)+ anon SELECT). 쓰기인ensure_naver_facility는 열지 않았다.검증
마이그레이션은 프로덕션에 rollback 트랜잭션으로 13개 항목을 확인한 뒤 적용했다 — 마스크 형식, 동의·인증 거부, 같은 번호 재사용,
app.uid()차단 대uid_lite()허용, visit_no 1→2, 인증 만료 시reverify_required, 승격 후 표시명 전환과 회차 유지, 정식 계정 중복 가입 거부.배포 후 실물 확인:
302 /r/<facilityId>·facebookexternalhit→ OG 태그 정상(링크 미리보기 유지)signup-lite검증 경로 — 동의 없이 400privacy_consent_required, 없는 코드로 400code_mismatch_or_expiredensure_naver_facility는 42501 유지주의
supabase db push를 쓰지 말 것. 마이그레이션 이력 테이블이 실제와 어긋나 있어 이미 적용된 6개를 재실행하려 든다. psql 직접 적용했다.anon_security_definer_function_executable경고가 개방한 함수 수만큼 다시 뜬다. 의도된 것이니 advisor 정리 때 도로 잠그지 말 것(마이그레이션 주석에 근거를 남겼다).미검증
send-phone-code의purpose='review'는 호출하면 실제 SMS 가 나가 테스트하지 않았다. 목적 화이트리스트 문자열 추가와 DB CHECK 뿐이고 CHECK 는 확인했다.남은 일
0029.md설계 문서와docs/supabase-api.md의signup-lite항목.🤖 Generated with Claude Code