Skip to content

간이 회원(lite) 후기 + 매장 QR 후기 직행 + 비로그인 시설 조회 - #132

Open
seizeh wants to merge 4 commits into
mainfrom
feat/share-view-redirect
Open

간이 회원(lite) 후기 + 매장 QR 후기 직행 + 비로그인 시설 조회#132
seizeh wants to merge 4 commits into
mainfrom
feat/share-view-redirect

Conversation

@seizeh

@seizeh seizeh commented Jul 27, 2026

Copy link
Copy Markdown
Owner

매장에 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_userphone_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 는 RLS using (true) + anon SELECT). 쓰기인 ensure_naver_facility 는 열지 않았다.

검증

마이그레이션은 프로덕션에 rollback 트랜잭션으로 13개 항목을 확인한 뒤 적용했다 — 마스크 형식, 동의·인증 거부, 같은 번호 재사용, app.uid() 차단 대 uid_lite() 허용, visit_no 1→2, 인증 만료 시 reverify_required, 승격 후 표시명 전환과 회차 유지, 정식 계정 중복 가입 거부.

배포 후 실물 확인:

  • 사람 UA → 302 /r/<facilityId> · facebookexternalhit → OG 태그 정상(링크 미리보기 유지)
  • signup-lite 검증 경로 — 동의 없이 400 privacy_consent_required, 없는 코드로 400 code_mismatch_or_expired
  • anon 으로 시설 287건·후기 5건 조회, ensure_naver_facility 는 42501 유지

주의

  • ⚠️ supabase db push 를 쓰지 말 것. 마이그레이션 이력 테이블이 실제와 어긋나 있어 이미 적용된 6개를 재실행하려 든다. psql 직접 적용했다.
  • ⚠️ Supabase 린터의 anon_security_definer_function_executable 경고가 개방한 함수 수만큼 다시 뜬다. 의도된 것이니 advisor 정리 때 도로 잠그지 말 것(마이그레이션 주석에 근거를 남겼다).
  • 이미 프로덕션에 적용·배포된 상태다. 이 PR 은 사후 기록이다.

미검증

send-phone-codepurpose='review' 는 호출하면 실제 SMS 가 나가 테스트하지 않았다. 목적 화이트리스트 문자열 추가와 DB CHECK 뿐이고 CHECK 는 확인했다.

남은 일

0029.md 설계 문서와 docs/supabase-api.mdsignup-lite 항목.

🤖 Generated with Claude Code

seizeh and others added 4 commits July 27, 2026 16:45
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>
@seizeh

seizeh commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

추가: 설계 문서 + API 레퍼런스 (85102c8)

0029.md — 코드만 봐서는 안 보이는 결정들을 남겼습니다.

  • status='lite' 를 고른 이유 — app.uid()active 를 요구하므로 기본이 차단이 됩니다. 화이트리스트 방식이었다면 새 RPC 마다 빠뜨릴 위험이 생깁니다.
  • 승격이 선택이 아닌 이유 — users_phone_uq 때문에 승격이 없으면 간이로 후기를 쓴 사람이 정식 가입을 아예 못 합니다.
  • 마스크를 nickname 컬럼에 넣으면 안 되는 이유 — 조합 1,000가지 대 유니크 제약.
  • QR 대상이 업체 계정이 아니라 시설인 이유, 그리고 business_profiles 에 주인 없는 행을 미리 만들지 말라는 경고.

0028 §배경의 퍼널 설명이 사실과 달랐다는 것도 기록했습니다. "스캔 → 설치 → 전화번호 인증 → 위치 동의 → 지역 인증 → 후기 작성" 이라고 적혀 있지만, add_facility_review 에는 지역·위치 조건이 한 줄도 없고 트리거도 전부 AFTER 입니다. 실제 마찰은 설치와 가입 둘뿐이었습니다.

docs/supabase-api.mdsignup-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)는 용도와 설계 문서만 표로 명시했습니다 — 읽지 않은 코드를 지어내지 않으려고요. 나머지 항목은 여전히 7/02 시점이라는 것도 헤더에 적었습니다.

READMEsupabase db push 금지 경고를 넣었습니다. 이번 배포에서 실제로 확인한 문제이고, 다음에 누가 무심코 돌리면 이미 적용된 6개를 재실행합니다. psql 직접 적용과 rollback 사전 검증 절차를 적어뒀습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant